爬虫
爬虫是定义如何抓取特定网站(或一组网站)的类,包括如何执行爬取(即跟踪链接)以及如何从其页面中提取结构化数据(即抓取项目)。换句话说,爬虫是您定义特定网站(或在某些情况下是一组网站)的爬取和解析页面的自定义行为的地方。
对于爬虫,抓取周期大致如下
首先,您生成初始请求以爬取第一个 URL,并指定一个回调函数,以便在从这些请求下载响应后调用该函数。
要执行的第一个请求是通过迭代
start()方法获得的,该方法默认会为start_urls爬虫属性中的每个 URL 生成一个Request对象,并将parse方法设置为callback函数以处理每个Response。在回调函数中,您解析响应(网页)并返回 项目对象、
Request对象,或这些对象的可迭代对象。这些 Request 也将包含一个回调(可能相同),然后由 Scrapy 下载,其响应再由指定的回调函数处理。在回调函数中,您通常使用 选择器(但您也可以使用 BeautifulSoup、lxml 或任何您喜欢的机制)解析页面内容,并使用解析后的数据生成项目。
尽管这个周期或多或少适用于任何类型的爬虫,但 Scrapy 中捆绑了不同类型的默认爬虫以用于不同目的。我们将在本文中讨论这些类型。
scrapy.Spider
- class scrapy.spiders.Spider
- class scrapy.Spider(*args: Any, **kwargs: Any)[source]
任何爬虫都必须继承的基类。
它提供了一个默认的
start()实现,该实现根据start_urls类属性发送请求,并为每个响应调用parse()方法。- name
一个字符串,定义此爬虫的名称。爬虫名称是 Scrapy 定位(并实例化)爬虫的方式,因此它必须是唯一的。但是,这不妨碍您实例化同一爬虫的多个实例。这是最重要的爬虫属性,并且是必需的。
如果爬虫抓取单个域名,通常的做法是将爬虫命名为该域名,无论是否包含 顶级域名(TLD)。例如,一个抓取
mywebsite.com的爬虫通常会被命名为mywebsite。
- allowed_domains
一个可选的字符串列表,包含此爬虫被允许抓取的域名。如果启用了
OffsiteMiddleware,则不属于此列表中指定域名(或其子域名)的 URL 请求将不会被跟踪。假设您的目标 URL 是
https://www.example.com/1.html,那么请将'example.com'添加到列表中。
- crawler
此属性由
from_crawler()类方法在类初始化后设置,并链接到此爬虫实例所绑定的Crawler对象。Crawler 封装了项目中的许多组件,以实现其单点访问(例如扩展、中间件、信号管理器等)。有关它们的更多信息,请参阅 Crawler API。
- state
一个字典,可用于在批次之间持久化一些爬虫状态。有关更多信息,请参阅 在批次之间保持持久状态。
- from_crawler(crawler, *args, **kwargs)[source]
这是 Scrapy 用来创建您的爬虫的类方法。
您可能不需要直接覆盖此方法,因为默认实现充当
__init__()方法的代理,使用给定的位置参数args和命名参数kwargs调用它。尽管如此,此方法在新实例中设置了
crawler和settings属性,以便稍后在爬虫代码中访问它们。2.11 版本变更: 现在可以在此方法中修改
crawler.settings中的设置,如果您想根据参数修改它们,这会很方便。因此,这些设置不是最终值,因为它们可能会在稍后被例如 附加组件 修改。出于同样的原因,大多数Crawler属性在此阶段尚未初始化。最终设置和已初始化的
Crawler属性在start()方法、engine_started信号的处理程序以及后续阶段中可用。- 参数:
crawler (
Crawler实例) – 爬虫将绑定的 Crawlerargs (list) – 传递给
__init__()方法的参数kwargs (dict) – 传递给
__init__()方法的关键字参数
- classmethod update_settings(settings)[source]
update_settings()方法用于修改爬虫的设置,并在爬虫实例初始化期间调用。它将一个
Settings对象作为参数,可以添加或更新爬虫的配置值。此方法是一个类方法,这意味着它在Spider类上调用,并允许爬虫的所有实例共享相同的配置。虽然可以在
custom_settings中设置每个爬虫的设置,但使用update_settings()允许您根据其他设置、爬虫属性或其他因素动态添加、删除或更改设置,并使用除'spider'之外的设置优先级。此外,在子类中通过覆盖update_settings()可以轻松扩展它,而对custom_settings执行相同的操作可能会很困难。例如,假设一个爬虫需要修改
FEEDSimport 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)
- async start() AsyncIterator[Any][source]
生成要发送的初始
Request对象。2.13 版本中新增。
例如
from scrapy import Request, Spider class MySpider(Spider): name = "myspider" async def start(self): yield Request("https://toscrape.com/")
默认实现从
start_urls读取 URL,并为每个 URL 生成一个启用dont_filter的请求。它在功能上等同于async def start(self): for url in self.start_urls: yield Request(url, dont_filter=True)
您也可以生成 项目。例如
async def start(self): yield {"foo": "bar"}
要编写适用于低于 2.13 版本的 Scrapy 的爬虫,还需要定义一个返回可迭代对象的同步
start_requests()方法。例如def start_requests(self): yield Request("https://toscrape.com/")
另请参阅
- parse(response)[source]
这是 Scrapy 用于处理已下载响应的默认回调,当其请求未指定回调时使用。
parse方法负责处理响应并返回抓取到的数据和/或更多要跟踪的 URL。其他 Request 回调函数与Spider类具有相同的要求。此方法以及任何其他 Request 回调函数必须返回一个
Request对象、一个 项目对象、一个包含Request对象和/或 项目对象 的可迭代对象,或者None。- 参数:
response (
Response) – 要解析的响应
- closed(reason)
在爬虫关闭时调用。此方法为
spider_closed信号提供了 signals.connect() 的快捷方式。
让我们看一个示例
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 和项目
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() 而不是 start_urls;为了使数据更有结构,您可以使用 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)
爬虫参数
爬虫可以接收修改其行为的参数。爬虫参数的一些常见用途是定义起始 URL 或将爬取限制在网站的特定部分,但它们可以用于配置爬虫的任何功能。
爬虫参数通过 crawl 命令使用 -a 选项传递。例如
scrapy crawl myspider -a category=electronics
爬虫可以在其 __init__ 方法中访问参数
import scrapy
class MySpider(scrapy.Spider):
name = "myspider"
def __init__(self, category=None, *args, **kwargs):
super(MySpider, self).__init__(*args, **kwargs)
self.start_urls = [f"http://www.example.com/categories/{category}"]
# ...
默认的 __init__ 方法将获取任何爬虫参数并将其作为属性复制到爬虫中。上面的示例也可以写成如下形式
import scrapy
class MySpider(scrapy.Spider):
name = "myspider"
async def start(self):
yield scrapy.Request(f"http://www.example.com/categories/{self.category}")
如果您 从脚本运行 Scrapy,可以在调用 CrawlerProcess.crawl 或 CrawlerRunner.crawl 时指定爬虫参数
process = CrawlerProcess()
process.crawl(MySpider, category="electronics")
请记住,爬虫参数仅是字符串。爬虫不会自行进行任何解析。如果您要从命令行设置 start_urls 属性,则必须使用 ast.literal_eval() 或 json.loads() 等方法将其解析为列表,然后将其设置为属性。否则,您将导致迭代 start_urls 字符串(一个非常常见的 Python 陷阱),从而导致每个字符都被视为一个单独的 URL。
爬虫参数也可以通过 Scrapyd schedule.json API 传递。参见 Scrapyd 文档。
scrapy-spider-metadata 参数
传递爬虫参数的另一种替代方法是使用库 scrapy-spider-metadata。
这允许 Scrapy 爬虫将其参数定义、验证、文档化和预处理为 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(),
}
此爬虫可以从命令行调用
scrapy crawl bookspider -a pages=2
起始请求
起始请求 是从爬虫的 start() 方法或 爬虫中间件 的 process_start() 方法中生成的 Request 对象。
另请参阅
延迟起始请求迭代
您可以如下覆盖 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
这有助于在任何给定时间最大程度地减少调度器中的请求数量,从而最大程度地减少资源使用(内存或磁盘,具体取决于 JOBDIR)。
通用爬虫
Scrapy 附带了一些有用的通用爬虫,您可以从中继承您的爬虫。它们旨在为一些常见的抓取情况提供便利功能,例如根据特定规则跟踪网站上的所有链接、从 Sitemap 抓取,或解析 XML/CSV 提要。
对于以下爬虫中使用的示例,我们将假设您有一个项目,其中在 myproject.items 模块中声明了一个 TestItem
from dataclasses import dataclass
@dataclass
class TestItem:
id: str | None = None
name: str | None = None
description: str | None = None
CrawlSpider
- class scrapy.spiders.CrawlSpider[source]
这是用于爬取常规网站最常用的爬虫,因为它通过定义一组规则提供了方便的链接跟踪机制。它可能不是最适合您特定网站或项目的爬虫,但它足够通用,适用于多种情况,因此您可以从它开始,并根据需要进行覆盖以实现更多自定义功能,或者只是实现自己的爬虫。
除了从 Spider 继承的属性(您必须指定)之外,此类还支持一个新属性
- rules
它是一个包含一个(或多个)
Rule对象的列表。每个Rule定义了爬取网站的特定行为。规则对象将在下面描述。如果有多个规则匹配同一个链接,则将根据它们在此属性中定义的顺序使用第一个规则。
此爬虫还公开了一个可覆盖的方法
爬取规则
- class scrapy.spiders.Rule(link_extractor: LinkExtractor | None = None, callback: CallbackT | str | None = None, cb_kwargs: dict[str, Any] | None = None, follow: bool | None = None, process_links: ProcessLinksT | str | None = None, process_request: ProcessRequestT | str | None = None, errback: Callable[[Failure], Any] | str | None = None)[source]
link_extractor是一个 链接提取器(Link Extractor) 对象,它定义了如何从每个爬取页面中提取链接。每个生成的链接将用于生成一个Request对象,该对象将在其meta字典中(在link_text键下)包含链接的文本。如果省略,将使用一个不带参数创建的默认链接提取器,从而提取所有链接。callback是一个可调用对象或一个字符串(在这种情况下,将使用爬虫对象中具有该名称的方法),用于为使用指定链接提取器提取的每个链接调用。此回调接收一个Response作为其第一个参数,并且必须返回一个单独的实例或一个 项目对象 和/或Request对象(或它们的任何子类)的可迭代对象。如上所述,接收到的Response对象将在其meta字典中(在link_text键下)包含生成Request的链接文本。cb_kwargs是一个字典,包含要传递给回调函数的关键字参数。follow是一个布尔值,指定是否应从此规则提取的每个响应中跟踪链接。如果callback为 None,则follow默认为True,否则默认为False。process_links是一个可调用对象或一个字符串(在这种情况下,将使用爬虫对象中具有该名称的方法),它将为使用指定link_extractor从每个响应中提取的每个链接列表调用。这主要用于过滤目的。process_request是一个可调用对象(或一个字符串,在这种情况下,将使用爬虫对象中具有该名称的方法),它将为该规则提取的每个Request调用。此可调用对象应将该请求作为第一个参数,并将请求来源的Response作为第二个参数。它必须返回一个Request对象或None(以过滤掉请求)。errback是一个可调用对象或一个字符串(在这种情况下,将使用爬虫对象中具有该名称的方法),用于在处理由规则生成的请求时引发任何异常时调用。它接收一个Twisted Failure实例作为第一个参数。警告
由于其内部实现,在编写基于
CrawlSpider的爬虫时,您必须为新请求显式设置回调;否则可能会发生意外行为。
CrawlSpider 示例
现在让我们看一个带有规则的 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
这个爬虫将开始爬取 example.com 的主页,收集类别链接和项目链接,并使用 parse_item 方法解析后者。对于每个项目响应,将使用 XPath 从 HTML 中提取一些数据,并用这些数据填充一个 Item。
XMLFeedSpider
- class scrapy.spiders.XMLFeedSpider[source]
XMLFeedSpider 旨在通过按特定节点名称迭代 XML 提要来解析它们。迭代器可以从
iternodes、xml和html中选择。出于性能原因,建议使用iternodes迭代器,因为xml和html迭代器会一次性生成整个 DOM 以进行解析。然而,在解析标记不佳的 XML 时,使用html作为迭代器可能会很有用。要设置迭代器和标签名称,您必须定义以下类属性
- iterator
一个字符串,定义要使用的迭代器。它可以是
它默认为:
'iternodes'。
- itertag
一个字符串,包含要迭代的节点(或元素)的名称。例如
itertag = 'product'
- namespaces
一个
(prefix, uri)元组列表,定义此爬虫将处理的文档中可用的命名空间。prefix和uri将用于使用register_namespace()方法自动注册命名空间。然后,您可以在
itertag属性中指定带有命名空间的节点。示例:
class YourSpider(XMLFeedSpider): namespaces = [('n', 'http://www.sitemaps.org/schemas/sitemap/0.9')] itertag = 'n:url' # ...
除了这些新属性之外,此爬虫还具有以下可覆盖的方法
- adapt_response(response)[source]
一个方法,它在响应从爬虫中间件到达后,爬虫开始解析之前立即接收响应。它可用于在解析响应主体之前修改它。此方法接收一个响应,并也返回一个响应(可以是相同或不同的响应)。
- parse_node(response, selector)[source]
此方法为匹配提供的标签名称(
itertag)的节点调用。它接收响应和每个节点的Selector。覆盖此方法是强制性的。否则,您的爬虫将无法工作。此方法必须返回一个 项目对象、一个Request对象,或者一个包含其中任何一个的可迭代对象。
- process_results(response, results)[source]
此方法为爬虫返回的每个结果(项目或请求)调用,旨在在将结果返回到框架核心之前执行任何所需的最后处理,例如设置项目 ID。它接收一个结果列表和产生这些结果的响应。它必须返回一个结果列表(项目或请求)。
警告
由于其内部实现,在编写基于
XMLFeedSpider的爬虫时,您必须为新请求显式设置回调;否则可能会发生意外行为。
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
基本上,我们上面所做的是创建一个爬虫,它从给定的 start_urls 下载提要,然后迭代其每个 item 标签,将它们打印出来,并将一些随机数据存储在一个 Item 中。
CSVFeedSpider
- class scrapy.spiders.CSVFeedSpider[source]
此爬虫与 XMLFeedSpider 非常相似,不同之处在于它迭代的是行,而不是节点。每次迭代中调用的方法是
parse_row()。- delimiter
一个字符串,包含 CSV 文件中每个字段的分隔符。默认为
','(逗号)。
- quotechar
一个字符串,包含 CSV 文件中每个字段的包围字符。默认为
'"'(引号)。
- headers
CSV 文件中列名的列表。
CSVFeedSpider 示例
让我们看一个与前一个类似的示例,但使用 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
- class scrapy.spiders.SitemapSpider[source]
SitemapSpider 允许您通过使用 Sitemap 发现 URL 来抓取网站。
它支持嵌套 Sitemap 并从 robots.txt 中发现 Sitemap URL。
- sitemap_urls
一个 URL 列表,指向您要抓取的 Sitemap 文件中的 URL。
您也可以指向 robots.txt 文件,它将被解析以从中提取 Sitemap URL。
- sitemap_rules
一个元组列表
(regex, callback),其中regex是一个用于匹配从 Sitemap 中提取的 URL 的正则表达式。regex可以是字符串或已编译的正则表达式对象。callback 是用于处理匹配正则表达式的 URL 的回调函数。
callback可以是一个字符串(指示爬虫方法的名称)或一个可调用对象。
例如
sitemap_rules = [('/product/', 'parse_product')]
规则按顺序应用,只有第一个匹配的规则会被使用。
如果您省略此属性,Sitemap 中找到的所有 URL 都将使用
parse回调函数进行处理。
- sitemap_follow
一个 Sitemap 正则表达式列表,应遵循这些 Sitemap。这仅适用于使用指向其他 Sitemap 文件的 Sitemap 索引文件 的网站。
默认情况下,所有 Sitemap 都会被跟踪。
- sitemap_alternate_links
指定是否应跟踪一个
url的备用链接。这些是同一网站中不同语言的链接,在相同的url块内传递。例如
<url> <loc>http://example.com/</loc> <xhtml:link rel="alternate" hreflang="de" href="http://example.com/de"/> </url>
设置了
sitemap_alternate_links后,这将检索两个 URL。禁用sitemap_alternate_links后,将只检索http://example.com/。默认情况下,
sitemap_alternate_links是禁用的。
- sitemap_filter(entries)[source]
这是一个过滤函数,可以被覆盖以根据 Sitemap 条目的属性选择它们。
例如
<url> <loc>http://example.com/</loc> <lastmod>2005-01-01</lastmod> </url>
我们可以定义一个
sitemap_filter函数,按日期过滤entriesfrom 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 年及以后修改的
entries。条目是从 Sitemap 文档中提取的字典对象。通常,键是标签名称,值是标签内的文本。
需要注意的是
由于 loc 属性是必需的,没有此标签的条目将被丢弃
备用链接存储在一个列表中,键为
alternate(参见sitemap_alternate_links)命名空间被移除,因此名为
{namespace}tagname的 lxml 标签仅变为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,使用不同回调函数处理其他 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.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 ...