1. 创建第一个项目

76 阅读11分钟

创建项目

在要创建项目的文件夹下,使用python -m scrapy startproject tutorial命令创建项目,其中python -m scrapy startproject为固定的写法,tutorial为项目的名字,这个命令会在当前目录下创建出tutorial文件夹,目录结构如下

tutorial
|   scrapy.cfg          # 部署配置文件
|
\---tutorial            # 项目的Python模块,从这里导入代码
    |   items.py        # 项目项定义文件
    |   middlewares.py  # 项目中间件文件
    |   pipelines.py    # 项目管道文件
    |   settings.py     # 项目设置文件
    |   __init__.py     
    |
    \---spiders         # 稍后放置spiders的目录
            __init__.py

创建spider

进入项目目录,例如这里的tutorial目录(注意是第一层的目录),然后执行python -m scrapy genspider quotes_spider quotes.toscrape.com, 命令行会进行类似这样的输出

C:\tutorial>python -m scrapy genspider quotes_spider quotes.toscrape.com
Created spider 'quotes_spider' using template 'basic' in module:
  tutorial.spiders.quotes_spider

现在tutorial/tutorial/spiders目录下面会多出来一个quotes_spider.py的文件, 内容如下,每行的含义可以看注释

import scrapy


class QuotesSpiderSpider(scrapy.Spider):
    # 爬虫的名字
    name = "quotes_spider"
    # 当前爬虫允许访问的域名,不符合的会自动屏蔽掉
    allowed_domains = ["quotes.toscrape.com"]
    # 起始域名,第一个访问的url
    start_urls = ["https://quotes.toscrape.com"]

    # 解析数据的方法
    def parse(self, response):
        pass

修改spider

quotes_spider.py的内容替换为下面的内容

from pathlib import Path

import scrapy


class QuotesSpider(scrapy.Spider):
    name = "quotes"

    def start(self):
        urls = [
            "https://quotes.toscrape.com/page/1/",
            "https://quotes.toscrape.com/page/2/",
        ]
        for url in urls:
            yield scrapy.Request(url=url, callback=self.parse)

    def parse(self, response):
        # 从URL中提取页码(如 "page/1/" → "1")
        page = response.url.split("/")[-2]
        # 生成文件名(如 quotes-1.html)
        filename = f"quotes-{page}.html"
        # 将响应内容保存为HTML文件
        Path(filename).write_bytes(response.body)
        # 日志记录(输出到控制台)
        self.log(f"Saved file {filename}")

注意,这里的爬虫名类名都改变了,还需要注意的是start方法,这个方法在2.13之前为start_requests,默认实现与代码中的实现一致,区别在于默认读取的urls列表为start_urls,所以这里的写法等效于不写start方法,直接声明数组start_urls = [ "https://quotes.toscrape.com/page/1/", "https://quotes.toscrape.com/page/2/", ],两种方式相较而言,实现start方法会更加灵活,更容易定制;使用start_urls会更简洁,适合不需要特殊处理的脚本

等效于:

from pathlib import Path

import scrapy


class QuotesSpider(scrapy.Spider):
    name = "quotes"
    start_urls = [
        "https://quotes.toscrape.com/page/1/",
        "https://quotes.toscrape.com/page/2/",
    ]

    def parse(self, response):
        page = response.url.split("/")[-2]
        filename = f"quotes-{page}.html"
        Path(filename).write_bytes(response.body)

start方法中使用了yield关键词,这个是python中的生成器,生成器是一种特殊的迭代器,可以自动实现__iter__() 和 __next__()两个方法,yield 暂停函数执行,保留局部变量

运行spider

在项目的顶层目录下,运行python -m scrapy crawl quotes执行代码,这个命令中quotes为spider中对应子类中的name的值,其他部分为固定写法

方法执行完毕后,在顶层目录下会出现quotes-1.htmlquotes-2.html两个文件,即请求的结果

解析结果

交互式解析

scrapy提供了一个shell的命令,提供了交互式的操作,同时也便于进行开发和调试

命令格式为python -m scrapy shell <url>,这里的url可以是网络地址,如https://xxxx.xx这种,也可以是本地文件,例如./quotes-1.html,也可以是完整地址,例如file:///C:/tutorial/quotes-1.html。当为网络地址时,需要用双引号包裹

运行之后进入交互终端, 使用--nolog可以跳过不需要的日志输出,如果是在项目目录中运行这个命令,会以项目中的setting.py作为配置

可用快捷键

  • shelp() - 打印包含可用对象列表的帮助
  • fetch(url[, redirect=True]) - 从给定的 URL 获取新响应 并相应地更新所有相关对象。可以选择redirect=False要求HTTP 3xx重定向后不传递
  • fetch(request) - 从给定的请求中获取新响应并更新 所有相关对象。
  • view(response) - 在本地web浏览器中打开给定的响应以供检查。这将在响应体中添加一个标签,以便外部链接(如图像和样式表)正确显示。但是请注意,这将在您的计算机中创建一个临时文件,该文件不会自动删除。

可用的 Scrapy 对象

Scrapy shell会自动从下载的页面创建一些方便的对象,如Response对象和Selector对象(用于HTML和XML内容)。

这些对象是:

  • crawler - 当前爬虫对象。
  • spider - 已知处理该URL的Spider,或者如果当前URL没有找到Spider,则为Spider对象
  • request - 最后一个提取页面的Request对象。您可以使用replace()修改此请求,也可以使用shortcut.fetch获取新请求(无需离开shell)
  • response - 一个包含最后提取页面的Response对象
  • settings - 当前的 Scrapy 设置

常见的操作

css表达式

可以使用response.css("title")获取title标签,这会返回一个 ​SelectorList 对象​(类似列表),包含所有匹配的 <title> 标签

使用response.css("title::text").getall()获取title标签的文本,其中getall()表示获取所有符合要求的文本;

使用get()来获取第一个, 等效于[0].get(),例如response.css("title::text")[0].get()。但是使用get()如果元素不存在,会返回None,而不会报错

除了getall()get()之外,还可以用正则表达式进行操作,例如

>>> response.css("title::text").re(r"Quotes.*")
['Quotes to Scrape']
>>> response.css("title::text").re(r"Q\w+")
['Quotes']
>>> response.css("title::text").re(r"(\w+) to (\w+)")
['Quotes', 'Scrape']
xpath表达式

可以使用response.xpath("//title")获取title标签

使用response.xpath("//title/text()").get()来获取文本,大致操作与css选择器相似,但是表达式的写法与方法不同

除了getall(),get()之外,在1.x的早期版本中还有一组对应的方法extract()和extract_first(),新版本中也可以用,但是推荐用新的api

脚本解析

在交互式中的操作,在脚本中同样可以执行,例如response.css("div.quote")等,但是脚本中需要使用变量来接受,修改quotes_spider.py的内容如下

import scrapy


class QuotesSpider(scrapy.Spider):
    name = "quotes"
    start_urls = [
        "https://quotes.toscrape.com/page/1/",
        "https://quotes.toscrape.com/page/2/",
    ]

    def parse(self, response):
        # 找到所有class为quote的div并进行遍历
        for quote in response.css("div.quote"):
            yield {
                # 找到div中class为text的span,并获取第一个标签的文本
                "text": quote.css("span.text::text").get(),
                # 找到div中class为author的small标签,并获取第一个的文本
                "author": quote.css("small.author::text").get(),
                # 找到div中所有class为tags的div下面的所有class为tag的a标签,并获取所有a标签的文本
                "tags": quote.css("div.tags a.tag::text").getall(),
            }

保存,然后使用python -m scrapy crawl quotes命令运行,在输出的日志中可以看到类似{'text': '“It is not a lack of love, but a lack of friendship that makes unhappy marriages.”', 'author': 'Friedrich Nietzsche', 'tags': ['friendship', 'lack-of-friendship', 'lack-of-love', 'love', 'marriage', 'unhappy-marriage']} 的输出,证明我们写的表达式确实正常解析出来了想要的内容

关于表达式的内容,这个要分析html的文档结构,然后编写对应的表达式来获取

比如打开网页的的源码进行分析,通过class或者id或者属性等来定位元素,然后进行处理

存储结果

简单存储

解析之后的结果可以使用python -m scrapy crawl quotes -O quotes.json命令进行存储,运行后在项目根目录会生成一个quotes.json的文件,打开之后就是解析到的数据

这个命令中python -m scrapy crawl quotes为运行命令,-O为覆盖写入的意思,quotes.json为写入文件的文件名,除了-O之外,还有-o参数,为追加写入的意思,当使用-O-o参数时,会激活内置的导出器,将parse方法yield的数据保存到文件,具体的导出器由扩展名决定,除了json,还支持.json.jsonl.csv.xml.pickle.marshal 等(根据 Scrapy 版本支持情况),还可以使用-t参数,用来指定输出格式,优先级高于文件扩展名,例如 -t json

管道存储

管道是一个类,通常会命名为XxxPipeline,当项目比较简单的时候,类可以直接被定义在pipelines.py中;当项目很复杂,或者管道类很复杂需要拆分的时候,则可以将类单独用一个文件存储,只需要在settings.py文件中指定清楚即可

spiderparse方法中返回的对象,会被传递到管道中,按照管道的顺序依次处理,管道的典型用途包括:

  • 清理 HTML 数据
  • 验证抓取的数据(检查项目是否包含某些字段)
  • 检查重复项(并删除它们)
  • 将抓取的项目存储在数据库中

在项目的pipelines.py文件中,可以看到一个默认创建出来的TutorialPipeline

创建一个管道需要在类中实现process_item(self, item, spider)方法,这个方法中对每个对象进行处理,itemspiderparse()方法返回的数据,spider则是处理数据的类,也就是之前写的QuotesSpider

当对象被处理完毕之后则使用return返回,如果数据不需要进一步的处理直接丢弃,则使用raise返回,例如下面这个例子

from itemadapter import ItemAdapter
from scrapy.exceptions import DropItem


class PricePipeline:
    vat_factor = 1.15

    def process_item(self, item, spider):
        adapter = ItemAdapter(item)
        if adapter.get("price"):
            if adapter.get("price_excludes_vat"):
                adapter["price"] = adapter["price"] * self.vat_factor
            return item
        else:
            raise DropItem("Missing price")

DropItem会被标记为丢弃,不会被传递到后续的管道,日志中会记录传递的信息,例如这里的Missing price

除了process_item方法外,还可以重写open_spider(self, spider)方法和close_spider(self, spider)方法,这两个方法分别在spider被打开和关闭时调用,参数中的spider即spider实例

例如下面这个 存储数据到文件 的例子

import json

from itemadapter import ItemAdapter


class JsonWriterPipeline:
    def open_spider(self, spider):
        self.file = open("items.jsonl", "w")

    def close_spider(self, spider):
        self.file.close()

    def process_item(self, item, spider):
        line = json.dumps(ItemAdapter(item).asdict()) + "\n"
        self.file.write(line)
        return item

这个管道将数据存入items.jsonl

创建管道

这里使用创建文件的方式,而不是直接在pipelines.py中新增类

在项目的spiders目录的同级中新建目录pipelines,然后创建json_writer_pipeline.py文件,将上面的 存储数据到文件 的例子粘贴进去

要使管道生效,还需要在settings.py中配置,配置项为ITEM_PIPELINES,例如

ITEM_PIPELINES = {
    "tutorial.pipelines.json_writer_pipeline.JsonWriterPipeline": 300
}

前面的key为类在项目中的定位,后面的数字为运行顺序,一般为从0-1000范围之内,数字越小的越先执行

运行python -m scrapy crawl quotes,运行结束后在项目根目录下可以看到items.jsonl文件

如果有多个管道需要执行,在配置中配置多个即可

这个例子仅用于展示管道的使用,如果需要将数据转化为json文件保存,参考之前的简单存储代码

获取全部数据

通过递归,不停的获取下一页,一直到获取全部数据

修改quotes_spider.py的内容为如下代码

import scrapy  # 导入Scrapy框架

class QuotesSpider(scrapy.Spider):
    # 爬虫的基本属性
    name = "quotes"  # 爬虫的唯一标识名(通过命令运行:scrapy crawl quotes)
    start_urls = [
        "https://quotes.toscrape.com/page/1/",  # 初始爬取的URL列表(可多个)
    ]

    def parse(self, response):
        """
        处理网页响应的核心方法,会被自动调用
        Args:
            response: 下载器返回的网页响应对象(包含HTML内容)
        """
        # 遍历页面中所有class="quote"的div元素
        for quote in response.css("div.quote"):
            # 提取每条名言的信息,生成字典格式数据
            yield {
                "text": quote.css("span.text::text").get(),      # 提取名言文本(CSS选择器)
                "author": quote.css("small.author::text").get(), # 提取作者(::text表示文本内容)
                "tags": quote.css("div.tags a.tag::text").getall(), # 提取所有标签(getall()返回列表)
            }

        # 查找"下一页"的链接(通过CSS选择器获取href属性)
        next_page = response.css("li.next a::attr(href)").get()
        
        # 如果存在下一页
        if next_page is not None:
            # 将相对URL转为绝对URL(如/page/2/ → https://quotes.toscrape.com/page/2/)
            next_page = response.urljoin(next_page)
            
            # 创建新的请求,指定回调函数为parse(实现自动翻页)
            yield scrapy.Request(next_page, callback=self.parse)

这段代码的的变化就是下面多了个获取下一页,如果存在下一页,就将相对url转化为绝对url,然后调用当前方法继续获取,核心代码yield scrapy.Request(next_page, callback=self.parse),实现递归获取数据的操作

follow方式获取

如果需要请求的地址不是相对路径,或者和当前主域名不一致,则可以修改下面部分的代码为

next_page = response.css("li.next a::attr(href)").get()
if next_page is not None:
    yield response.follow(next_page, callback=self.parse)

response.follow是用来生成新的请求,可以自动处理 相对/绝对 url,并且可以直接传入Selector ,并且代码更简洁,需要注意的是去重​:Scrapy 默认会过滤重复 URL,若分页参数不同但内容相同(如 ?page=2 和 ?page=2#top),需设置 dont_filter=True。例如yield response.follow(a, callback=self.parse, dont_filter=True)

只有页码的获取

如果分页栏只有页数,而没有下一页,或者希望同时请求页码而不是一页一页的获取,可以使用下面循环的代码进行爬取

for href in response.css("ul.pager a::attr(href)"):
    yield response.follow(href, callback=self.parse)

更简洁的写法,这种写法不必获取href,而是直接传入Selector对象,但是限制条件是a标签的链接必须放在href属性中

for a in response.css("ul.pager a"):
    yield response.follow(a, callback=self.parse)

还可以直接生成全部,而不是一个个的遍历

anchors = response.css("ul.pager a")
yield from response.follow_all(anchors, callback=self.parse)

更精简的写法

yield from response.follow_all(css="ul.pager a", callback=self.parse)

传递参数

在运行时动态传递的参数,可以使用-a参数,例如python -m scrapy crawl quotes -O quotes-humor.json -a tag=humor

在spider中,可以使用self.tag来获取,例如

import scrapy


class QuotesSpider(scrapy.Spider):
    name = "quotes"

    async def start(self):
        url = "https://quotes.toscrape.com/"
        tag = getattr(self, "tag", None)
        if tag is not None:
            url = url + "tag/" + tag
        yield scrapy.Request(url, self.parse)

    def parse(self, response):
        for quote in response.css("div.quote"):
            yield {
                "text": quote.css("span.text::text").get(),
                "author": quote.css("small.author::text").get(),
            }

        next_page = response.css("li.next a::attr(href)").get()
        if next_page is not None:
            yield response.follow(next_page, self.parse)

getattr() 是一个 ​内置函数,用于 ​动态获取对象的属性或方法。如果属性存在,则返回属性,否则返回默认值,这里的默认值为None