2.5 LinkExtractor 与 CrawlSpider:自动跟页


文档摘要

2.5 LinkExtractor 与 CrawlSpider:自动跟页 本节摘要:当"发现链接就跟进"成为规律而非例外,手写 yield 就该升级成规则声明。LinkExtractor 负责从页面筛链接,CrawlSpider 按 Rule 自动续票,回调只需要处理详情页。本节讲规则引擎的写法、过滤参数的调法,以及它与普通 Spider 的选型边界。 上一节的翻页还是手写的:找到下一页的地址,yield,等响应,再找。当站点有列表翻页加分类页加详情页三层结构时,手写续票代码会迅速膨胀成一堆互相调用的回调。CrawlSpider 把这件事翻转过来:你声明"什么样的链接跟进、跟到了交给谁",续票本身由框架代劳。这个思路的来源是 2.

2.5 LinkExtractor 与 CrawlSpider:自动跟页

本节摘要:当"发现链接就跟进"成为规律而非例外,手写 yield 就该升级成规则声明。LinkExtractor 负责从页面筛链接,CrawlSpider 按 Rule 自动续票,回调只需要处理详情页。本节讲规则引擎的写法、过滤参数的调法,以及它与普通 Spider 的选型边界。

上一节的翻页还是手写的:找到下一页的地址,yield,等响应,再找。当站点有列表翻页加分类页加详情页三层结构时,手写续票代码会迅速膨胀成一堆互相调用的回调。CrawlSpider 把这件事翻转过来:你声明"什么样的链接跟进、跟到了交给谁",续票本身由框架代劳。这个思路的来源是 2.2 节那张 Request 车票——续票动作太规律了,规律的事就该交给机器。

两个角色:筛选器与规则

LinkExtractor 是筛选器:从响应里抽链接、按参数过滤。Rule 是调度声明:匹配到哪类链接、交给哪个回调、是否递归跟进。

from scrapy.linkextractors import LinkExtractor from scrapy.spiders import CrawlSpider, Rule class BookSiteSpider(CrawlSpider): name = "booksite" allowed_domains = ["books.example.com"] start_urls = ["https://books.example.com/index"] rules = ( # 规则一:分类页。匹配 category 路径,跟进后继续套用全部规则 Rule(LinkExtractor(allow=r"/category/\d+"), follow=True), # 规则二:翻页。匹配 page 参数,只负责跟进,不产出数据 Rule(LinkExtractor(allow=r"/list.*page=\d+"), follow=True), # 规则三:详情页。匹配 book 路径,交回调拆包,不再递归 Rule(LinkExtractor(allow=r"/book/\d+"), callback="parse_book"), ) def parse_book(self, response): yield { "title": response.css("h1::text").get(default="").strip(), "price": response.css("p.price::text").get(default=""), "url": response.url, }

注意 CrawlSpider 不再使用默认的 parse 作为入口——parse 被规则引擎占用了。所有数据提取写在规则指定的回调里。运行日志能看到规则引擎的工作量:

DEBUG: Crawled (200) books.example.com/index (referer: None) DEBUG: Crawled (200) books.example.com/category/3 (referer: index) DEBUG: Crawled (200) books.example.com/list?cat=3&page=2 (referer: category/3) DEBUG: Crawled (200) books.example.com/book/4521 (referer: list?cat=3&page=2) ... 'item_scraped_count': 340, 'offsite/filtered': 12, 'linkextractor/no_link': 0

offsite/filtered 是域名白名单拦下的越界链接数——allowed_domains 在这里继续生效。

LinkExtractor 的过滤参数

筛选器的力量全在参数上,常用五个:

LinkExtractor( allow=(r"/book/\d+",), # 白名单正则:地址必须匹配 deny=(r"/tag/",), # 黑名单正则:匹配即剔除 allow_domains=("books.example.com",), # 域名白名单 restrict_css=("div.pagination",), # 只在这个区块里找链接,范围收窄 unique=True, # 同页去重 )

restrict_css 是性价比最高的参数:翻页链接只在分页区块里找,模板里其他长得像的地址全部免疫。参数从宽到窄依次收拢,比写一个巨大的正则好维护得多。

规则匹配的优先级细节

多条规则按声明顺序匹配,先中先用。所以"越具体的规则越靠前":详情页规则必须排在翻页规则之前,否则一个同时匹配两类正则的地址会被错误分流。规则声明的顺序就是分流优先级,这一点没有隐式补救。

⚠️ 常见坑:在 CrawlSpider 里定义了 parse 方法却没被调用,是规则没匹配到链接,不是回调写错。用 scrapy shell 先确认链接的完整形态,再回来核对该写 allow 还是 restrict_css——相对地址会被框架自动补全域名后才参与匹配。

选型:普通 Spider 还是 CrawlSpider

场景 建议 理由
列表页加详情页两层、结构规整 CrawlSpider 规则一写,翻页免费
需要精细控制每类请求的参数 普通 Spider 手写 yield 才能逐条定制 meta 与 priority
链接结构混乱、需要边拆边判断 普通 Spider 声明式规则表达不了复杂判断
全站爬取、只求覆盖 CrawlSpider 规则加白名单即收敛

我的习惯是先用 CrawlSpider 铺骨架,遇到需要逐请求定制的地方(如某些详情页要 POST、要带特殊 meta),再把那一类链接改回普通 Spider 手写。两者在同一个工程里可以共存,按站点复杂度分配。

变式:给规则引擎加一道人工闸门

纯声明式规则偶尔力不从心:某些链接要不要跟,得看页面上别的信息。Rule 留了 process_links 钩子,每批候选链接先经你过目再出票:

def filter_links(self, links): # links 是本轮筛出的全部候选,逐个检查或丢弃 kept = [] for link in links: if "flag=ad" in link.url: # 广告位链接丢弃 continue kept.append(link) return kept rules = ( Rule(LinkExtractor(allow=r"/book/\d+"), callback="parse_book", process_links="filter_links"), )

它还适合做一件实用事:把 Spider 级的判断结果(比如当前会话有权限访问哪些分类)注入候选链接的 meta 里,跟下去的回调就能直接取用——规则引擎与业务判断的握手点,就在这里。

本节要点回顾

  • LinkExtractor 筛链接、Rule 定分流:声明式续票,回调只管拆包;
  • 五个过滤参数从宽到窄:allow、deny、域名、区块、页内去重,restrict_css 最实用;
  • 规则顺序即优先级:越具体的越靠前,parse 这个名字被规则引擎占用;
  • 选型看控制粒度:规整站点用规则,精细定制回手写,两者可共存。

到此,Spider 车间的工序齐了:印票、拆包、登记、续票。下一章车票进站排队——去看看候车大厅里的调度器、去重指纹与全局配置。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U