 与去重指纹机制)
Scrapling Spider 框架深入解析Request 对象、response.follow() 与去重指纹机制【免费下载链接】Scrapling️ An adaptive Web Scraping framework that handles everything from a single request to a full-scale crawl!项目地址: https://gitcode.com/GitHub_Trending/sc/Scrapling在 Scrapling 的 spiders爬虫框架模块中Request对象是整个爬取流程的调度单元它决定访问哪个 URL、由哪个回调处理响应、以什么优先级入队、以及是否会被去重过滤器丢弃。本文基于仓库中的参考文档 requests-responses.md结合 Request 类源码、Response.follow() 实现 与 Scheduler 调度器完整讲解请求的构造、链接跟随、回调约定、优先级调度与基于指纹的去重机制帮助你写出可控、可预测、可断点恢复的爬虫。Request 对象爬取流程的调度单元一个Request表示一个待抓取的 URL。你可以直接构造也可以在回调中通过response.follow()派生from scrapling.spiders import Request # 直接构造 request Request( https://example.com/page, callbackself.parse_page, priority5, ) # 通过 response.follow在回调中推荐的方式 request response.follow(/page, callbackself.parse_page)Request的全部构造参数如下与 源码 中的__init__签名一致参数类型默认值说明urlstr必填要抓取的 URLsidstr会话 ID将请求路由到指定会话见 Sessions 文档callbackcallableNone处理响应的异步生成器方法默认为parse()priorityint0值越大越先被处理dont_filterboolFalse为True时跳过去重允许重复请求metadict{}任意元数据会透传到对应的 response 上**kwargs额外关键字参数直接传给会话的 fetch 方法如headers、method、data、proxy等从源码看**kwargs被存为self._session_kwargs见 request.py#L51这是后续两件事的基础一是它们会被原样转发给底层 fetcher二是参与请求指纹计算从而影响去重行为后文详述。由于 kwargs 直接透传发起 POST 请求只需多传两个参数yield Request( https://example.com/api, methodPOST, data{key: value}, callbackself.parse_result, )此外Request还内置了几个值得了解的细节domain属性从 URL 解析域名request.py#L67-L69引擎用它来做allowed_domains过滤和每域名并发限制copy()方法复制请求meta做浅拷贝被引擎用于被封锁请求的重试逻辑pickle 支持__getstate__/__setstate__在序列化时把callback替换为其方法名字符串request.py#L153-L174反序列化后通过_restore_callback()从 spider 实例上找回方法——这正是断点checkpoint机制能够把待处理请求持久化到磁盘并恢复的关键。Response.follow()回调中创建后续请求的推荐方式response.follow()是回调中创建后续请求的推荐方式。相比直接构造Request它有四个优势相对 URL自动相对当前页面 URL 解析源码中通过self.urljoin(url)实现Referer 头默认被设置为当前页面 URL原请求的会话 kwargsheaders、proxy 等会被继承callback、sid、priority在未显式指定时从原请求继承。async def parse(self, response: Response): # 最简形式 - 继承当前请求的 callback、sid、priority yield response.follow(/next-page) # 覆盖特定字段 yield response.follow( /product/123, callbackself.parse_product, priority10, ) # 传递额外的元数据 yield response.follow( /details, callbackself.parse_details, meta{category: electronics}, )response.follow()的参数表如下参数类型默认值说明urlstr必填要跟随的 URL绝对或相对sidstr会话 ID为空时继承原请求callbackcallableNone回调方法为None时继承原请求priorityintNone优先级为None时继承原请求dont_filterboolFalse跳过去重注意此参数不继承metadictNone元数据与当前 response 的 meta 合并referer_flowboolTrue将当前 URL 设置为 Referer 头**kwargs与原请求的会话 kwargs 合并新值优先源码级拆解follow() 到底做了什么阅读 custom.py#L88-L144 中Response.follow()的实现可以看到完整的继承与合并逻辑# 合并原会话 kwargs 与新 kwargs新值优先 session_kwargs {**self.request._session_kwargs, **kwargs} if referer_flow: # 针对静态请求引擎 headers session_kwargs.get(headers, {}) headers[referer] self.url session_kwargs[headers] headers # 针对浏览器引擎 extra_headers session_kwargs.get(extra_headers, {}) extra_headers[referer] self.url session_kwargs[extra_headers] extra_headers session_kwargs[google_search] False return Request( urlself.urljoin(url), sidsid or self.request.sid, callbackcallback or self.request.callback, prioritypriority if priority is not None else self.request.priority, dont_filterdont_filter, meta{**(self.meta or {})}, **(meta or {})}, **session_kwargs, )这段实现揭示了三个文档层面看不到的细节双引擎兼容的 Referer 注入referer_flowTrue时会同时写入headers供Fetcher静态引擎和extra_headers供浏览器引擎两个字典把当前响应 URL 作为referer确保无论底层用哪种会话请求都会带上正确的 Referer副作用google_search False启用 referer flow 时会强制关闭google_search选项——即不再把带 Referer 的请求先经 Google 中转避免中转逻辑干扰链接跟随前置校验如果 response 上没有关联请求self.request不是Request实例follow()会直接抛出TypeError。这意味着follow()只能在 spider 回调中使用而不能在独立使用 fetcher 拿到 response 后随意调用。禁用 Referer 流转默认情况下response.follow()会把Referer头设置为当前页面 URL。如果目标站点对 Referer 敏感例如某些反爬策略会校验来源页可以显式关闭yield response.follow(/page, referer_flowFalse)同时注意referer_flowFalse也不会写入google_searchFalse两个行为是绑定在一起的。回调Callbacks约定异步生成器与三种产出类型回调是 spider 上处理响应的异步生成器方法它必须yield以下三种类型之一dict一个抓取到的条目加入结果集Request一个后续请求加入调度队列None被静默忽略。class MySpider(Spider): name my_spider start_urls [https://example.com] async def parse(self, response: Response): # 产出条目dict yield {url: response.url, title: response.css(title::text).get()} # 产出后续请求 for link in response.css(a::attr(href)).getall(): yield response.follow(link, callbackself.parse_page) async def parse_page(self, response: Response): yield {content: response.css(article::text).get()}注意所有回调方法都必须是async def且使用yield而非return。即使某个回调只产出条目、没有后续请求它也必须是异步生成器。Spider 基类 中的抽象方法签名async def parse(...) - AsyncGenerator[Dict | Request | None, None]正是这一约定的类型化表达。从引擎侧的实现 engine.py#L154-L183 可以进一步看到回调产出物的完整处理链路callback request.callback if request.callback else self.spider.parse async for result in callback(response): if isinstance(result, Request): if self._is_domain_allowed(result): self._normalize_request(result) await self.scheduler.enqueue(result) else: self.stats.offsite_requests_count 1 elif isinstance(result, dict): processed_result await self.spider.on_scraped_item(result) ... elif result is not None: log.error(fSpider must return Request, dict or None, ...)由此可以确认几处文档未展开但实际会生效的行为allowed_domains过滤产出的Request会先经过域名白名单检查站外请求会被计入offsite_requests_count而不入队on_scraped_item钩子每个dict条目都会先经过该钩子返回None即可静默丢弃该条目计入items_dropped异常兜底回调内部抛出的异常会被捕获、记日志并触发on_error(request, error)不会中断整个爬取。另外_normalize_requestengine.py#L145-L152会在入队前把空sid解析为会话管理器的默认会话 ID保证同一会话的请求指纹一致。请求优先级值越大越先处理优先级高的请求先被处理这在需要先抓重点页、后翻分页的场景下非常有用async def parse(self, response: Response): # 高优先级 - 先处理商品页 for link in response.css(a.product::attr(href)).getall(): yield response.follow(link, callbackself.parse_product, priority10) # 低优先级 - 分页链接在商品页之后处理 next_page response.css(a.next::attr(href)).get() if next_page: yield response.follow(next_page, callbackself.parse, priority0)使用response.follow()时若未显式指定priority它会继承原请求的优先级源码中是priority if priority is not None else self.request.priority。调度器如何实现优先级Scheduler 基于asyncio.PriorityQueue实现入队时把优先级取反# 优先级取反使数值更大者先出队counter 保证同优先级 FIFO item (-request.priority, counter, request) await self._queue.put(item)配套地Request定义了__lt__/__gt__比较优先级request.py#L133-L143并用单调递增计数器打破同优先级之间的平局保证先入队者先出队。测试用例 test_request.py#L186-L202 对这两处比较语义有直接验证。一个容易忽略的细节当请求被判定为被封锁并重试时引擎会把重试请求的priority - 1engine.py#L250使重试不会插队到同优先级的正常请求之前。去重与指纹Fingerprint机制spider 会基于一个指纹自动去重请求指纹由URL、HTTP 方法、请求体与会话 ID计算得出两个请求指纹相同第二个会被静默丢弃Scheduler.enqueue 中通过self._seen集合判断。需要允许重复请求时例如登录后重新访问同一页面设置dont_filterTrueyield Request(https://example.com/dashboard, dont_filterTrue, callbackself.parse_dashboard) # 或用 response.follow yield response.follow(/dashboard, dont_filterTrue, callbackself.parse_dashboard)指纹到底算了什么Request.update_fingerprint() 展示了完整的指纹算法post_data self._session_kwargs.get(data, {}) body b if post_data: if isinstance(post_data, dict | list | tuple): body urlencode(post_data).encode() elif isinstance(post_data, str): body post_data.encode() elif isinstance(post_data, BytesIO): body post_data.getvalue() elif isinstance(post_data, bytes): body post_data else: post_data self._session_kwargs.get(json, {}) body orjson.dumps(post_data) if post_data else b data { sid: self.sid, body: body.hex(), method: self._session_kwargs.get(method, GET), url: canonicalize_url(self.url, keep_fragmentskeep_fragments), } # ...可选kwargs / headers fp hashlib.sha1(orjson.dumps(data, optionorjson.OPT_SORT_KEYS), usedforsecurityFalse).digest()由此可确认几个要点URL 归一化使用 w3lib 的canonicalize_url()对 URL 做规范化因此http://ex.com/a?x1y2与参数顺序不同的等价 URL 会命中同一指纹默认会丢弃 URL 片段#section请求体参与计算datadict/list/tuple 会urlencodestr/bytes/BytesIO 原样编码或在无data时的json参数都会被编码进指纹。也就是说同一 URL 的 GET 与 POST或 body 不同的两次 POST视为不同请求会话隔离sid是指纹的一部分同一 URL 经由不同会话请求时不会互相去重结果缓存首次计算后缓存在self._fp后续比较直接复用__eq__基于指纹比较未计算指纹时比较会抛RuntimeError测试见 test_request.py#L202-L216。三个指纹微调开关可以通过 spider 类属性微调指纹的组成定义于 spider.py#L96-L98由 CrawlerEngine 转传给 Scheduler属性默认值作用fp_include_kwargsFalse把额外请求 kwargs传给会话 fetch 的参数如 headers 以外的method、cookies等纳入指纹fp_keep_fragmentsFalse计算指纹时保留 URL 片段#sectionfp_include_headersFalse把请求 headers 纳入指纹例如需要把https://example.com/page#section1与https://example.com/page#section2视为两个不同 URL 时class MySpider(Spider): name my_spider fp_keep_fragments True # ...fp_include_kwargs的实现细节request.py#L106-L112值得注意它会把除data/json外的 kwargs 键名小写化值经orjson稳定序列化键排序再按排序后的(key, value)元组参与哈希——因此 kwargs 的书写顺序不影响指纹而data/json因已体现在body中被排除避免重复计算。fp_include_headers则把headers或extra_headers中的键值各自转 bytes 后取 hex 参与哈希键名统一小写、但保留值的大小写见 test_request.py#L127 的验证。指纹还有一个工程上的用途开启development_mode时响应缓存以指纹为 key 落盘engine.py#L199-L209因此同一个请求在开发模式下会直接回放缓存响应而不真正发起网络请求。用 Request.meta 在回调之间传递上下文meta字典允许你在回调之间传递任意数据这在从列表页提取上下文再到详情页使用的场景中非常有用async def parse(self, response: Response): for product in response.css(div.product): category product.css(span.category::text).get() link product.css(a::attr(href)).get() if link: yield response.follow( link, callbackself.parse_product, meta{category: category}, ) async def parse_product(self, response: Response): yield { name: response.css(h1::text).get(), price: response.css(.price::text).get(), # 从请求中读取 meta category: response.meta.get(category, ), }使用response.follow()时新 meta 会与当前 response 的 meta合并且新值优先源码中为{**(self.meta or {}), **(meta or {})}见 custom.py#L142。这与直接构造Request时 meta 被整体替换的行为不同是跟随式请求继承上下文语义的一部分。spider 体系还会自动写入一些元数据。例如启用代理轮换proxy/proxies参数时各引擎会在响应上附带所用代理静态引擎在 static.py#L258 处以meta{proxy: proxy}构造响应浏览器引擎在 _controllers.py#L191 与 _stealth.py#L279 处做同样处理。因此可以在回调里通过response.meta[proxy]读取本次请求实际使用的代理便于排查哪个代理触发了封锁。小结Request的七个显式参数加**kwargs透传覆盖了 URL、会话路由、回调、优先级、去重、元数据与会话参数response.follow()在相对 URL 解析、Referer 注入、会话 kwargs 继承、meta 合并上省去了大量样板代码且referer_flowFalse可一键关闭 Referer 流转回调必须是async defyield产出dict/Request/None引擎负责域名过滤、条目钩子与异常兜底优先级数值越大越先调度底层靠asyncio.PriorityQueue 取反优先级实现同优先级 FIFO去重指纹 归一化 URL 方法 请求体 sidSHA-1可用dont_filter豁免可用fp_include_kwargs/fp_keep_fragments/fp_include_headers三个类属性微调meta是回调间传递上下文的唯一正式通道同时引擎会自动写入proxy等系统元数据。完整的 spiders 文档体系见仓库 agent-skill/Scrapling-Skill/references/spiders/ 目录其中 sessions.md 讲解sid背后的会话机制architecture.md 讲解引擎整体架构可与本文互为补充。【免费下载链接】Scrapling️ An adaptive Web Scraping framework that handles everything from a single request to a full-scale crawl!项目地址: https://gitcode.com/GitHub_Trending/sc/Scrapling创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考