Scrapy Spiders 深度指南:Spider 基类、Spider 参数、Start Requests 与内置通用 Spiders
Spider(爬虫)是 Scrapy 中定义“如何抓取一个站点或一组站点”的核心类:它决定发送哪些请求、如何解析响应以提取数据,并据此发出后续请求。理解 Spider 的基类属性(name、allowed_domains、start_urls、custom_settings)、Spider 参数传递机制(-a 选项)、Start requests 的生命周期(惰性迭代、异常处理),以及 CrawlSpider、XMLFeedSpider、CSVFeedSpider、SitemapSpider 四类内置通用 Spider,是编写任何 Scrapy 爬虫项目的基础。本文基于 Scrapy 仓库的官方文档 docs/topics/spiders.rst 展开,并结合 scrapy/spiders/ 下的源码实现给出纵深解析。
Spider 与抓取流程
Spider 是 Scrapy 架构中位于 Spider Middlewares 之下的组件,由引擎(Engine)驱动。一个完整的抓取流程如下(与 docs/topics/spiders.rst 中的描述一致):
- Scrapy 迭代 Spider 的
start()方法来获取初始请求。默认实现会为start_urls中的每个 URL 产出一个scrapy.Request对象,并以parse方法作为回调(callback)。 - Scrapy 下载每个请求,并用产生的
scrapy.http.Response调用其回调。 - 回调解析响应(通常使用 Selector),返回或
yieldItem 对象(携带提取的数据)以及用于继续抓取的scrapy.Request,这些请求回到第 2 步。 - Item 经过 Item Pipeline,通常最终通过 Feed Exports 落盘存储。
scrapy.Spider 基类
所有 Spider 都必须继承 Spider 基类。从源码看,其定义位于 scrapy/spiders/init.py#L33,文档中的完整 API 描述在 docs/topics/spiders.rst。
类属性
name:定义 Spider 名称的字符串。Spider 名称是 Scrapy 定位(并实例化)Spider 的依据,因此必须唯一——这是 Spider 最重要且必填的属性。如果 Spider 只抓取单个域名,常见做法是用域名(带或不带 TLD)命名,例如抓取 mywebsite.com 的 Spider 常命名为 mywebsite。从源码看,名称校验发生在构造函数中(scrapy/spiders/init.py#L47-L54):若既没有传入 name 也没有类属性 name,会抛出 ValueError。
allowed_domains:可选的域名列表,限定该 Spider 允许抓取的域名。只要 OffsiteMiddleware 处于启用状态,不在此列表(或其子域)中的 URL 对应的请求就不会被跟随。以目标 URL https://www.example.com/1.html 为例,列表中只需加入 'example.com'。自 2.18.0 起,抓取运行期间修改该属性也会生效,变更会影响之后调度的请求——例如可以动态允许“只从先前响应中才得知”的域名。
从源码看,OffsiteMiddleware 通过监听 request_scheduled 信号逐请求过滤(scrapy/downloadermiddlewares/offsite.py#L79-L101):每个请求调度时触发 process_request,若请求未设 dont_filter、meta 中未设 allow_offsite,且 should_follow() 判定为站外,则记录 offsite/filtered 统计并抛出 IgnoreRequest。注意子域匹配语义是精确的后缀匹配:www.example.org 允许 bob.www.example.org,但不允许 www2.example.org。
start_urls:起始 URL 列表。默认的 start() 实现会为其中每个 URL 发出请求。
custom_settings:一个字典,在运行该 Spider 时覆盖项目级配置。由于设置在实例化之前就已更新,它必须定义为类属性。内置设置完整清单见 Settings 参考。
crawler:由 from_crawler 类方法在类初始化后设置的属性,指向该 Spider 实例所绑定的 scrapy.crawler.Crawler 对象。Crawler 封装了项目的大量组件(extensions、middlewares、信号管理器等)供单一入口访问,详见 Crawler API。
settings:运行该 Spider 的配置,是一个 scrapy.settings.Settings 实例,详细介绍见 Settings 主题。
logger:用 Spider 的 name 创建的 Python logger,可通过它发送日志(见 日志文档)。从源码看,logger 实际是一个 property(scrapy/spiders/init.py#L56-L62),每次访问都会返回一个绑定了 spider 字段的 SpiderLoggerAdapter,因此日志中会自动带上 spider 上下文。
state:一个 dict,可用于在不同批次(batch)之间持久化 Spider 状态(与 JOBDIR 断点续爬机制配合,见 jobs 文档)。
from_crawler 类方法
from_crawler(crawler, *args, **kwargs) 是 Scrapy 用来创建你 Spider 的类方法。默认实现是 __init__ 的代理:用给定的 args 和 kwargs 调用 __init__,但会在新实例上设置 crawler 和 settings 属性,以便之后在 Spider 代码中访问。实现见 scrapy/spiders/init.py#L78-L87:
@classmethod
def from_crawler(cls, crawler, *args, **kwargs):
spider = cls(*args, **kwargs)
spider._set_crawler(crawler)
return spider
def _set_crawler(self, crawler):
self.crawler = crawler
self.settings = crawler.settings
crawler.signals.connect(self.close, signals.spider_closed)
自 2.11 起,from_crawler 中可以修改 crawler.settings(例如基于参数动态调整),方便按参数定制配置。但要注意:此时这些设置不是最终值,之后仍可能被 add-ons(add-ons 文档)修改;同理,Crawler 的大部分属性在此时还未初始化。最终设置和已初始化的 Crawler 属性要到 start 方法、engine_started 信号处理器及之后才可用。
update_settings 类方法
update_settings(settings) 用于修改 Spider 的设置,在 Spider 实例初始化期间被调用。它接收一个 Settings 对象,可以添加或更新该 Spider 的配置值。作为类方法,它作用于 Spider 类,允许该 Spider 的所有实例共享同一份配置。
与 custom_settings 相比,update_settings() 的优势是:可以基于其他设置、Spider 属性或其他因素动态地添加、删除、修改设置,并使用 'spider' 之外的设置优先级;在子类中扩展 update_settings() 也比覆盖 custom_settings 更容易。源码实现非常简洁(scrapy/spiders/init.py#L166-L168):
@classmethod
def update_settings(cls, settings):
settings.setdict(cls.custom_settings or {}, priority="spider")
即默认行为就是把 custom_settings 以 'spider' 优先级写入。示例——假设 Spider 需要修改 FEEDS:
import scrapy
class MySpider(scrapy.Spider):
name = "myspider"
custom_feed = {
"/home/user/documents/items.json": {
"format": "json",
"indent": 4,
}
}
@classmethod
def update_settings(cls, settings):
super().update_settings(settings)
settings.setdefault("FEEDS", {}).update(cls.custom_feed)
start 与 parse
start():异步生成器方法,yield 出初始的 Request 对象(也可以 yield Item)。文档给出的默认实现说明与源码完全一致(scrapy/spiders/init.py#L135-L136):
async def start(self):
for url in self.start_urls:
yield Request(url, dont_filter=True)
也就是说默认请求带有 dont_filter=True,起始 URL 不会被去重过滤器拦截。
parse(response, **kwargs):默认的回调方法。start() 产出的请求未显式指定 callback 时,响应会交给 parse 处理;请求的 cb_kwargs 会作为关键字参数传入。除非该 Spider 发出的每个请求都定义了 callback,否则必须定义 parse。源码中它直接抛出 NotImplementedError(scrapy/spiders/init.py#L145-L164),提示 parse callback is not defined。
closed(reason):Spider 关闭时被调用,等价于为 spider_closed 信号注册处理器。源码中 close 静态方法在 spider_closed 信号触发时查找并调用 Spider 上定义的 closed 可调用对象(scrapy/spiders/init.py#L174-L179)。
基础示例
一个最基础的 Spider(同时展示了 name、allowed_domains、start_urls、parse):
import scrapy
class MySpider(scrapy.Spider):
name = "example.com"
allowed_domains = ["example.com"]
start_urls = [
"http://www.example.com/1.html",
"http://www.example.com/2.html",
"http://www.example.com/3.html",
]
def parse(self, response):
self.logger.info("A response from %s just arrived!", response.url)
在一个回调中同时返回多个 Request 与 Item:
import scrapy
class MySpider(scrapy.Spider):
name = "example.com"
allowed_domains = ["example.com"]
start_urls = [
"http://www.example.com/1.html",
"http://www.example.com/2.html",
"http://www.example.com/3.html",
]
def parse(self, response):
for h3 in response.xpath("//h3").getall():
yield {"title": h3}
for href in response.xpath("//a/@href").getall():
yield scrapy.Request(response.urljoin(href), self.parse)
也可以不用 start_urls 而直接重写 start();为了给数据更强的结构,可以使用 scrapy.Item 对象:
import scrapy
from myproject.items import MyItem
class MySpider(scrapy.Spider):
name = "example.com"
allowed_domains = ["example.com"]
async def start(self):
yield scrapy.Request("http://www.example.com/1.html", self.parse)
yield scrapy.Request("http://www.example.com/2.html", self.parse)
yield scrapy.Request("http://www.example.com/3.html", self.parse)
def parse(self, response):
for h3 in response.xpath("//h3").getall():
yield MyItem(title=h3)
for href in response.xpath("//a/@href").getall():
yield scrapy.Request(response.urljoin(href), self.parse)
Spider 参数(Spider arguments)
Spider 可以接收修改其行为的外部参数。常见用途包括定义起始 URL、把抓取限制到站点的某些板块,但也可以用来配置 Spider 的任何功能。
命令行 -a 选项
Spider 参数通过 crawl 命令的 -a 选项传递,例如:
scrapy crawl myspider -a category=electronics
Spider 在 __init__ 方法中访问这些参数:
import scrapy
class MySpider(scrapy.Spider):
name = "myspider"
def __init__(self, category=None, *args, **kwargs):
super().__init__(*args, **kwargs)
self.start_urls = [f"http://www.example.com/categories/{category}"]
# ...
默认的 __init__ 方法会把任意 Spider 参数作为属性拷贝到 Spider 实例上(见 scrapy/spiders/init.py#L52 的 self.__dict__.update(kwargs)),因此上面的示例也可以不重写 __init__ 而直接写:
import scrapy
class MySpider(scrapy.Spider):
name = "myspider"
async def start(self):
yield scrapy.Request(f"http://www.example.com/categories/{self.category}")
从源码看,-a 选项在 BaseRunSpiderCommand 中定义并可重复使用(scrapy/commands/init.py#L189-L196),process_options 用 arglist_to_dict 把 NAME=VALUE 列表解析成字典(scrapy/commands/init.py#L214-L221),随后 scrapy/commands/crawl.py#L31 将其作为关键字参数展开传给 crawler_process.crawl():
self.crawler_process.crawl(self._create_crawler(spname), **opts.spargs)
最终这些参数经 from_crawler 进入 __init__ 的 **kwargs。
从脚本运行时传参
如果通过脚本运行 Scrapy,可以在调用 CrawlerProcess.crawl() 或 CrawlerRunner.crawl() 时指定 Spider 参数:
process = CrawlerProcess()
process.crawl(MySpider, category="electronics")
只接收字符串
务必记住:Spider 参数只可能是字符串。Spider 本身不会做任何解析。如果你想在命令行设置 start_urls 属性,必须自己用 ast.literal_eval 或 json.loads 之类的工具把它解析成列表再赋给属性;否则会导致对 start_urls 字符串做逐字符迭代(一个常见的 Python 陷阱),每个字符都被当作一个单独的 URL。
Spider 参数也可以经 Scrapyd 的 schedule.json API 传递。
scrapy-spider-metadata 参数
另一种传递 Spider 参数的选择是第三方库 scrapy-spider-metadata:它允许 Scrapy Spider 以 Pydantic 模型的方式定义、校验、记录并预处理参数。示例展示了带类型的参数——字符串参数会自动转换为整数:
import scrapy
from pydantic import BaseModel
from scrapy_spider_metadata import Args
class MyParams(BaseModel):
pages: int
class BookSpider(Args[MyParams], scrapy.Spider):
name = "bookspider"
start_urls = ["http://books.toscrape.com/catalogue"]
async def start(self):
for start_url in self.start_urls:
for index in range(1, self.args.pages + 1):
yield scrapy.Request(f"{start_url}/page-{index}.html")
def parse(self, response):
book_links = response.css("article.product_pod h3 a::attr(href)").getall()
for book_link in book_links:
yield response.follow(book_link, self.parse_book)
def parse_book(self, response):
yield {
"title": response.css("h1::text").get(),
"price": response.css("p.price_color::text").get(),
}
该 Spider 可以从命令行这样调用:
scrapy crawl bookspider -a pages=2
Start requests
Start requests 是从 Spider 的 start() 方法,或从 Spider 中间件的 process_start 方法(见 Spider Middleware)yield 出来的 Request 对象。
延迟 start 请求的迭代
Scrapy 以 yield 的速度尽可能快地迭代 start(),所以无论起始请求有多少,它们都会尽早全部进入调度器(Scheduler)。为了在任何时刻最小化调度器中的请求数量、以及由此带来的资源占用(内存,或使用 JOBDIR 时的磁盘占用),可以重写 start(),在存在已调度请求时暂停迭代:
async def start(self):
async for item_or_request in super().start():
if self.crawler.engine.needs_backout():
await self.crawler.signals.wait_for(signals.scheduler_empty)
yield item_or_request
源码级佐证:needs_backout() 定义于 scrapy/core/engine.py#L362,它汇总 Scraper 与 Downloader 两层的背压状态——Scraper 侧的判断是 active_size > max_active_size(scrapy/core/scraper.py#L102-L103),Downloader 侧则是总并发已满(scrapy/core/downloader/init.py#L127-L129)。当调度器取不出下一个请求时,引擎会发送 scheduler_empty 信号(scrapy/core/engine.py#L385-L388),signals.wait_for() 的实现见 scrapy/signalmanager.py#L104。两者配合即可实现“调度器空了才继续喂请求”的惰性流。
start 错误处理
start() 中抛出的异常会终止整个迭代,因此剩余的 start items 和 requests 永远不会被发送。Scrapy 会记录该异常日志、发送 spider_error 信号,并在已调度请求处理完毕后以 start_error 作为 finish_reason 统计值关闭 Spider。从源码看,引擎捕获 start 迭代中的异常后设置 _start_error = True(scrapy/core/engine.py#L292-L298),关闭时据此选择关闭原因(scrapy/core/engine.py#L612-L616):
default_reason = "start_error" if self._start_error else "finished"
(自 2.18.0 起行为如此:此前关闭原因是 finished,且既不发送 spider_error 信号也不上报 spider_exceptions/count 统计。)
要保持迭代继续,就自己捕获异常:
async def start(self):
for url in self.start_urls:
try:
request = Request(url)
except ValueError:
self.logger.exception(f"Skipping start URL {url}")
else:
yield request
若想直接停止抓取、并自定义 finish_reason,则抛出 scrapy.exceptions.CloseSpider。
内置通用 Spiders(Generic Spiders)
Scrapy 内置了几类通用 Spider,目标是针对几种常见抓取场景提供便利功能:按规则跟随站点所有链接、从 Sitemap 抓取、解析 XML/CSV feed。它们的源码分别位于 scrapy/spiders/crawl.py、scrapy/spiders/feed.py、scrapy/spiders/sitemap.py,并通过 scrapy/spiders/init.py#L186-L196 从 scrapy.spiders 统一导出。
以下示例假设项目中有一个 myproject.items 模块,声明了 TestItem:
from dataclasses import dataclass
@dataclass
class TestItem:
id: str | None = None
name: str | None = None
description: str | None = None
CrawlSpider
CrawlSpider 是抓取常规网站最常用的 Spider,它通过定义一组规则(rules)来提供便捷的链接跟随机制。它未必最适合你的特定网站或项目,但足够通用,你可以从它开始并按需覆盖,也可以直接自己实现 Spider。
除继承自 Spider 的属性外,它新增一个属性:
rules:一个由一个或多个Rule对象组成的列表。每个Rule定义一种抓取行为,Rule对象见下文。如果多条规则匹配同一个链接,按在rules中定义的顺序使用第一条匹配的规则。
Rule meta 键:从 rules 生成的请求会将其匹配规则在 rules 中的索引存入 Request.meta 的 rule 键。CrawlSpider 依赖该键把响应派发到正确的规则,因此把 rule 键复制到由其他规则生成的请求上,会导致响应被派发到错误的 callback。源码中这一机制一目了然(scrapy/spiders/crawl.py#L133-L139):
def _build_request(self, rule_index, link):
return Request(
url=link.url,
callback=self._callback,
errback=self._errback,
meta={"rule": rule_index, "link_text": link.text},
)
响应回来时 _callback 按 response.meta["rule"] 反查规则,再调用 parse_with_rules()(scrapy/spiders/crawl.py#L156-L163)。
此外 CrawlSpider 暴露一个可覆盖的方法:
parse_start_url(response, **kwargs):对start_urls中每个 URL 产生的响应调用,用于解析初始响应,必须返回 Item、Request或包含它们的可迭代对象。
抓取规则(Rule)各参数
Rule 构造函数签名见 scrapy/spiders/crawl.py#L64-L82,各参数含义:
link_extractor:一个 Link Extractor 对象,定义如何从每个抓取的页面提取链接。每个产出的链接都会生成一个Request,其中包含链接文本(存于meta字典的link_text键)。省略时使用无参创建的默认 Link Extractor,即提取所有链接(源码中为模块级单例_default_link_extractor)。callback:可调用对象或字符串(字符串表示 Spider 对象上同名方法),用于处理该链接提取器提取的每个链接,接收Response作为第一个参数,必须返回单个或可迭代的 Item 和/或Request(或其子类)对象。收到的Response的meta中带有产生该请求的链接文本(link_text键)。cb_kwargs:传给回调函数的关键字参数字典。follow:布尔值,指定是否跟随从该规则提取的每个响应中的链接。若callback为None,follow默认为True,否则默认为False——源码中即follow if follow is not None else not callback(scrapy/spiders/crawl.py#L82)。process_links:可调用对象或字符串(字符串表示 Spider 方法),对每个响应用指定link_extractor提取的链接列表调用,主要用于过滤目的。process_request:可调用对象(或字符串),对该规则提取的每个Request调用;第一个参数是该请求,第二个是产生它的Response;必须返回Request或None(用于过滤掉该请求)。- 可以用
process_request为规则生成的请求设置priority,例如process_request=lambda request, response: request.replace(priority=10)。
- 可以用
errback:可调用对象或字符串,当规则生成的请求处理过程中抛出异常时被调用,接收 TwistedFailure实例作为第一个参数。
警告:由于内部实现的原因,编写基于
CrawlSpider的 Spider 时,必须为新请求显式设置 callback,否则可能出现意外行为。
从源码看,规则在实例化时会被 _compile_rules 逐个浅拷贝并编译(把方法名字符串替换为真实方法,scrapy/spiders/crawl.py#L215-L220);链接去重逻辑在 _requests_to_follow 中用 seen 集合实现——同一个链接只会被最先匹配的规则处理(scrapy/spiders/crawl.py#L141-L154)。
CrawlSpider 示例
from scrapy.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor
class MySpider(CrawlSpider):
name = "example.com"
allowed_domains = ["example.com"]
start_urls = ["http://www.example.com"]
rules = (
# Extract links matching 'category.php' (but not matching 'subsection.php')
# and follow links from them (since no callback means follow=True by default).
Rule(LinkExtractor(allow=(r"category\.php",), deny=(r"subsection\.php",))),
# Extract links matching 'item.php' and parse them with the spider's method parse_item
Rule(LinkExtractor(allow=(r"item\.php",)), callback="parse_item"),
)
def parse_item(self, response):
self.logger.info("Hi, this is an item page! %s", response.url)
item = {}
item["id"] = response.xpath('//td[@id="item_id"]/text()').re(r"ID: (\d+)")
item["name"] = response.xpath('//td[@id="item_name"]/text()').get()
item["description"] = response.xpath(
'//td[@id="item_description"]/text()'
).get()
item["link_text"] = response.meta["link_text"]
url = response.xpath('//td[@id="additional_data"]/@href').get()
return response.follow(
url, self.parse_additional_page, cb_kwargs=dict(item=item)
)
def parse_additional_page(self, response, item):
item["additional_data"] = response.xpath(
'//p[@id="additional_data"]/text()'
).get()
return item
该 Spider 从 example.com 首页开始抓取,收集 category 链接与 item 链接,并用 parse_item 方法解析后者。对每个 item 响应,用 XPath 从 HTML 提取数据填充字典。
XMLFeedSpider
XMLFeedSpider 用于解析 XML feed,按特定节点名迭代。迭代器可选:iternodes、xml、html。出于性能考虑推荐使用 iternodes,因为 xml 和 html 迭代器会先生成整个 DOM 再解析。不过对标记不良(bad markup)的 XML,使用 html 迭代器可能有用。
必须定义的类属性:
-
iterator:定义使用的迭代器:'iternodes'—— 基于lxml的快速迭代器;'html'—— 使用scrapy.Selector的迭代器。注意它使用 DOM 解析、必须把全部 DOM 载入内存,大 feed 上可能是问题。它还会用 HTML 解析器解析 feed,可能静默破坏 HTML 视为空元素(void element)的标签(如<link>),丢弃其内容与结束标签——受此影响的 feed 请改用xml或iternodes;'xml'—— 使用scrapy.Selector的迭代器。同样使用 DOM 解析、全部 DOM 载入内存,大 feed 上可能是问题。
默认值:
'iternodes'(源码默认值见 scrapy/spiders/feed.py#L33)。 -
itertag:要迭代的节点(元素)名字符串,例如itertag = "product"。 -
namespaces:(prefix, uri)元组列表,定义文档中可用、将被本 Spider 处理的命名空间。prefix和uri会通过Selector.register_namespace方法自动注册命名空间,之后即可在itertag属性中指定带命名空间的节点:
from scrapy.spiders import XMLFeedSpider
class YourSpider(XMLFeedSpider):
namespaces = [("n", "http://www.sitemaps.org/schemas/sitemap/0.9")]
itertag = "n:url"
# ...
除新属性外,该 Spider 还有以下可覆盖方法(实现见 scrapy/spiders/feed.py#L37-L103):
adapt_response(response):响应从 Spider 中间件到达、Spider 开始解析之前调用。可用于解析前修改响应 body。接收一个响应并返回一个响应(可以相同,也可以是另一个)。parse_node(response, selector):对匹配itertag的每个节点调用,接收响应和该节点的scrapy.Selector。必须覆盖此方法,否则 Spider 不工作。必须返回 Item、Request或包含它们的可迭代对象。process_results(response, results):对 Spider 返回的每个结果(item 或 request)调用,用于在把结果交回框架核心前做最后的处理,例如设置 item ID。接收结果列表和产生这些结果的响应,必须返回结果列表。
警告:由于内部实现的原因,编写基于
XMLFeedSpider的 Spider 时,必须为新请求显式设置 callback,否则可能出现意外行为。
从源码看,三种迭代器分支在 _parse 中分发(scrapy/spiders/feed.py#L78-L98):iternodes 走 xmliter_lxml 流式迭代;xml/html 则构建完整 Selector 后执行 xpath(f"//{self.itertag}"),这解释了“大 feed 内存开销”警告的来源。xml 与 html 迭代器要求响应是 TextResponse,否则抛出 ValueError。
XMLFeedSpider 示例
from scrapy.spiders import XMLFeedSpider
from myproject.items import TestItem
class MySpider(XMLFeedSpider):
name = "example.com"
allowed_domains = ["example.com"]
start_urls = ["http://www.example.com/feed.xml"]
iterator = "iternodes" # This is actually unnecessary, since it's the default value
itertag = "item"
def parse_node(self, response, node):
self.logger.info(
"Hi, this is a <%s> node!: %s", self.itertag, "".join(node.getall())
)
item = TestItem()
item.id = node.xpath("@id").get()
item.name = node.xpath("name").get()
item.description = node.xpath("description").get()
return item
本质上,这个 Spider 从给定的 start_urls 下载 feed,然后迭代其中的每个 item 标签,打印并保存一些数据到 Item 中。相关测试可参考 tests/test_spider.py 中的 TestXMLFeedSpider(包括命名空间注册的用例)。
CSVFeedSpider
CSVFeedSpider 与 XMLFeedSpider 非常相似,只是迭代行(rows)而不是节点;每次迭代调用的方法是 parse_row。属性(见 scrapy/spiders/feed.py#L110-L157):
delimiter:CSV 文件中每个字段的分隔符,默认','(逗号)。quotechar:CSV 文件中每个字段的包裹(enclosure)字符,默认'"'(引号)。headers:CSV 文件列名列表。parse_row(response, row):接收响应和一个 dict(代表每一行),该 dict 以 CSV 文件提供(或检测到)的每个 header 为键。该 Spider 同样可以覆盖adapt_response和process_results做预处理与后处理。
从源码看,行迭代由 csviter 工具完成,parse_row 的返回值统一经 process_results 后交回框架(scrapy/spiders/feed.py#L148-L153):
def parse_rows(self, response):
for row in csviter(response, self.delimiter, self.headers, quotechar=self.quotechar):
ret = iterate_spider_output(self.parse_row(response, row))
yield from self.process_results(response, ret)
CSVFeedSpider 示例
from scrapy.spiders import CSVFeedSpider
from myproject.items import TestItem
class MySpider(CSVFeedSpider):
name = "example.com"
allowed_domains = ["example.com"]
start_urls = ["http://www.example.com/feed.csv"]
delimiter = ";"
quotechar = "'"
headers = ["id", "name", "description"]
def parse_row(self, response, row):
self.logger.info("Hi, this is a row!: %r", row)
item = TestItem()
item.id = row["id"]
item.name = row["name"]
item.description = row["description"]
return item
SitemapSpider
SitemapSpider 允许通过发现 Sitemap 中的 URL 来抓取站点。它支持嵌套 Sitemap,并能从 robots.txt 中发现 Sitemap URL。
sitemap_urls:指向要抓取其 URL 的 Sitemap 的 URL 列表。也可以指向robots.txt,此时会解析它以提取 Sitemap URL。sitemap_rules:(regex, callback)元组列表,其中regex是匹配从 Sitemap 提取的 URL 的正则(str 或编译后的 regex 对象),callback是处理匹配 URL 的回调(方法名字符串或可调用对象)。例如:
sitemap_rules = [("/product/", "parse_product")]
规则按顺序应用,只有第一条匹配的规则生效。省略该属性时,Sitemap 中发现的所有 URL 都会用 parse 回调处理(源码默认值即 [("", "parse")],见 scrapy/spiders/sitemap.py#L28-L30)。
sitemap_follow:应跟随的 Sitemap 的正则列表,仅针对使用 Sitemap index 文件(指向其他 Sitemap 文件)的站点。默认跟随所有 Sitemap(源码默认[""])。sitemap_alternate_links:指定是否跟随一个url的 alternate 链接(同一网站其他语言的链接,在同一个url块中传递)。例如:
<url>
<loc>http://example.com/</loc>
<xhtml:link rel="alternate" hreflang="de" href="http://example.com/de"/>
</url>
启用 sitemap_alternate_links 时会同时抓取两个 URL;禁用时只抓取 http://example.com/。默认禁用。
sitemap_filter(entries):可覆盖的过滤函数,按属性筛选 Sitemap 条目。条目是从 Sitemap 文档提取的 dict 对象,键通常是标签名,值是其中的文本。例如按日期过滤:
from datetime import datetime
from scrapy.spiders import SitemapSpider
class FilteredSitemapSpider(SitemapSpider):
name = "filtered_sitemap_spider"
allowed_domains = ["example.com"]
sitemap_urls = ["http://example.com/sitemap.xml"]
def sitemap_filter(self, entries):
for entry in entries:
date_time = datetime.strptime(entry["lastmod"], "%Y-%m-%d")
if date_time.year >= 2005:
yield entry
这将只抓取 2005 年及以后修改的条目。注意:
loc属性是必需的,没有该标签的条目会被丢弃;- alternate 链接以键
alternate存为列表(见sitemap_alternate_links); - 命名空间会被剥除,lxml 中形如
{namespace}tagname的标签名只剩tagname。
省略该方法时,Sitemap 中发现的所有条目都会被处理(仍遵循其他属性及其设置)。
SitemapSpider 示例
最简单的示例:用 parse 回调处理 Sitemap 发现的所有 URL:
from scrapy.spiders import SitemapSpider
class MySpider(SitemapSpider):
sitemap_urls = ["http://www.example.com/sitemap.xml"]
def parse(self, response):
pass # ... scrape item here ...
用不同回调处理不同 URL:
from scrapy.spiders import SitemapSpider
class MySpider(SitemapSpider):
sitemap_urls = ["http://www.example.com/sitemap.xml"]
sitemap_rules = [
("/product/", "parse_product"),
("/category/", "parse_category"),
]
def parse_product(self, response):
pass # ... scrape product ...
def parse_category(self, response):
pass # ... scrape category ...
跟随 robots.txt 中定义的 Sitemap,且只跟随 URL 包含 /sitemap_shop 的 Sitemap:
from scrapy.spiders import SitemapSpider
class MySpider(SitemapSpider):
sitemap_urls = ["http://www.example.com/robots.txt"]
sitemap_rules = [
("/shop/", "parse_shop"),
]
sitemap_follow = ["/sitemap_shops"]
def parse_shop(self, response):
pass # ... scrape shop here ...
把 SitemapSpider 与其他 URL 来源组合:
from scrapy import Request
from scrapy.spiders import SitemapSpider
class MySpider(SitemapSpider):
sitemap_urls = ["http://www.example.com/robots.txt"]
sitemap_rules = [
("/shop/", "parse_shop"),
]
other_urls = ["http://www.example.com/about"]
async def start(self):
async for item_or_request in super().start():
yield item_or_request
for url in self.other_urls:
yield Request(url, self.parse_other)
def parse_shop(self, response):
pass # ... scrape shop here ...
def parse_other(self, response):
pass # ... scrape other here ...
从源码看,SitemapSpider 的 start() 只需对 sitemap_urls 逐条发出指向 _parse_sitemap 的请求(scrapy/spiders/sitemap.py#L56-L58);_parse_sitemap 会自动识别 robots.txt(提取其中的 Sitemap URL)、Sitemap index(递归请求子 Sitemap)与 urlset(按 sitemap_rules 匹配并派发回调)三种形态(scrapy/spiders/sitemap.py#L69-L101)。此外它还内建了 gzip Sitemap 的解压逻辑(含基于 DOWNLOAD_MAXSIZE/DOWNLOAD_WARNSIZE 的解压大小保护,见 scrapy/spiders/sitemap.py#L119-L150),相关解析工具 Sitemap、sitemap_urls_from_robots 位于 scrapy/utils/sitemap.py。
小结
spiders包对外导出Spider、CrawlSpider、Rule、XMLFeedSpider、CSVFeedSpider、SitemapSpider六类(scrapy/spiders/init.py#L190-L197),覆盖了从“自定义规则式站点遍历”到“Sitemap/feed 驱动”的主要抓取形态。- 写 Spider 时牢记三条主线:用
name/allowed_domains/start_urls(或重写start())声明抓取范围;用-a参数或from_crawler注入运行期配置;用parse等回调产出 Item 与新Request驱动闭环。 - 需要站点级配置时优先
custom_settings,需要按条件动态改设置时覆盖update_settings();需要大规模起始请求时利用needs_backout()+scheduler_empty实现惰性迭代;start()中会失败的 URL 要自行 try/except,否则整条 start 迭代会提前终止。 - 更细粒度的行为(深度限制、Referer 处理、start 请求过滤等)由 Spider Middleware 层完成,见 Spider Middleware 文档;通用 Spider 的具体行为测试可参考 tests/test_spider.py、tests/test_spider_crawl.py 与 tests/test_spider_sitemap.py。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
