Python爬虫-scrapy框架

前言

难走的路,从不拥挤

好走的路,人山人海

⚠️声明:本文所涉及的爬虫技术及代码仅用于学习、交流与技术研究目的,禁止用于任何商业用途或违反相关法律法规的行为。若因不当使用造成法律责任,概与作者无关。请尊重目标网站的robots.txt协议及相关服务条款,共同维护良好的网络环境。

1.Scrapy框架

1.1简介

Scrapy 是一个开源的 Python 网络爬虫框架,用于从网站上抓取数据并进行处理。它可以高效、结构化地提取和存储网站信息。

Scrapy 的主要特点

  • 异步网络请求:Scrapy 基于 Twisted(Z一种异步网络库),能够同时处理多个 HTTP 请求,因此非常高效。
  • 内置数据导出功能:Scrapy 支持将抓取的数据导出为多种格式,如 JSON、CSV、XML 等。
  • 可定制的管道:可以通过自定义管道处理抓取的数据,例如清洗数据、存储到数据库等。
  • 选择器:Scrapy 提供了简洁的选择器系统,可以通过 XPath 和 CSS 轻松提取网页中的信息。
  • 用户代理伪装:Scrapy 支持模拟不同的用户代理,可以帮助绕过某些网站的反爬虫机制。

中文文档:https://docs.scrapy.net.cn/en/latest/

英文文档:https://docs.scrapy.org/en/latest/

image-20250131204303593

1.2异步和非阻塞

异步 vs 非阻塞

对比点 异步(Asynchronous) 非阻塞(Non-blocking)
核心概念 任务的执行方式 调用的行为
执行模型 任务可以在等待结果时执行其他任务 调用立即返回,不等待结果
是否需要回调 需要(callback、future、async/await) 可能需要轮询或回调
是否依赖事件驱动 不是必须
适用场景 适用于多任务并发,如爬虫、网络请求 适用于非阻塞 I/O,如非阻塞 socket

关系

  • 非阻塞 ≠ 异步,但异步通常是非阻塞的
  • 非阻塞 只是让调用立即返回,而异步 需要有一个调度机制(如事件循环)来管理任务的执行。
  • 非阻塞 I/O 可以用于异步编程,但异步编程还涉及任务的调度和回调处理。

1.2.1异步

异步(Asynchronous)

异步指的是任务的执行方式,意味着一个任务可以在等待结果的同时去执行其他任务,而不需要同步等待结果返回。

  • 任务的执行不按照顺序等待,而是由事件驱动的机制来决定何时执行。
  • 任务可能会挂起(yield)并在未来的某个时间点恢复执行。
  • 通常与回调(callback)、Promise/Futureasync/await 机制结合使用。

代码

import asyncio

async def fetch_data():
    print("开始获取数据")
    await asyncio.sleep(2)  # 模拟 IO 操作
    print("数据获取完成")
    return "数据"

async def main():
    task = asyncio.create_task(fetch_data())  # 异步执行
    print("做其他事情")
    await task  # 等待 fetch_data 结束

asyncio.run(main())

结果

开始获取数据
做其他事情
(等待2秒)
数据获取完成

在等待 fetch_data() 完成的同时,程序可以继续执行其他任务。

1.2.2非阻塞

非阻塞(Non-blocking)

非阻塞指的是调用方式,意味着调用一个函数时,它不会阻塞当前线程,而是立即返回,可以继续执行其他任务。

  • 任务的执行不按照顺序等待,而是由事件驱动的机制来决定何时执行。
  • 任务可能会挂起(yield)并在未来的某个时间点恢复执行。
  • 通常与回调(callback)、Promise/Futureasync/await 机制结合使用。

代码

import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setblocking(False)  # 设置非阻塞模式

try:
    sock.connect(('www.example.com', 80))
except BlockingIOError:
    print("非阻塞模式下,连接正在进行")

在非阻塞模式下,connect() 方法立即返回,而不会阻塞等待连接完成。

1.3Scrapy的工作流程

爬虫改的工作流程

image-20250131205518123

使用队列的爬虫

image-20250131205757089

Scrapy框架的执行流程

image-20250131205915990

2.Scrapy框架的安装

使用miniconda创建scrapy环境并安装

# 创建 Scrapy 环境
conda create -n scrapy_py3.12 python=3.12

# 激活环境
conda activate scrapy_py3.12

# 安装 Scrapy
# 或者使用 Conda 安装:conda install -c conda-forge scrapy
pip install scrapy

# 验证安装,输出 Scrapy 版本
scrapy version

# 创建 Scrapy 爬虫项目
# 在你的项目存放目录下执行创建项目
scrapy startproject myspider

# 进入项目目录
cd myspider

# 创建爬虫
# scrapy genspider 爬虫名 允许爬取的域名
scrapy genspider baidu baidu.com

# 运行爬虫文件
scrapy crawl baidu

创建 Scrapy 环境

image-20250131211347460

激活环境

image-20250131211416050

安装 Scrapy

image-20250131211545422

验证安装,输出 Scrapy 版本

image-20250131211603112

创建 Scrapy 爬虫项目,我是进入到了我自己的项目目录下创建的

image-20250131211732316

进入项目目录

image-20250131211905464

创建爬虫

image-20250131212010597

运行爬虫文件

image-20250131212112102

使用Pycharm打开项目,目录结果如下

image-20250131212300546

记得PyCharm设置解释器为刚才创建的scrapy_py3.12

image-20250131213303851

3.数据提取

蜻蜓FM地址: https://m.qingting.fm/rank/

浏览器使用移动端模式打开

image-20250131213925725

3.1创建项目

创建项目

# 创建项目目录
scrapy startproject fm
# 进入项目目录
cd fm
# 创建爬虫文件
scrapy genspider QingTingFM https://m.qingting.fm/rank/

image-20250131213843418

3.2parse方法

我们发现爬虫脚本QingTingFMSpider继承了scrapy.Spider

并且parseresponse的类型是scrapy.http.Response

parse还有一个参数**kwargs: Any

image-20250131214524724

Scrapy框架中可以使用cmdline.execute()方法来执行启动命令

if __name__ == '__main__':
    # 默认启动方式
    cmdline.execute("scrapy crawl QingTingFM".split())
    # 忽略日志信息
    # cmdline.execute("scrapy crawl QingTingFM --nolog".split())

3.3parse函数中的response参数

response 是 Scrapy 中的一个核心对象,它是从目标网站获取的 HTTP 响应,并且包含了网站返回的所有数据。这个对象包含许多有用的信息。以下是对 response 对象的详细解读:

  • response.url:返回请求的 URL 地址。
  • response.status:HTTP 响应状态码。用于指示请求是否成功,或者发生了什么错误。
  • response.headers:HTTP 响应头。它包含了许多与响应相关的元数据,例如内容类型、日期、服务器信息等。
  • response.body:返回的响应体(即页面内容),它通常是 HTML 或其他类型的内容(如 JSON、XML、图片等)。
  • response.request:返回原始请求的 Request 对象。可以用于访问原始的请求头、URL 等信息。
  • response.meta:这是一个字典,通常用于存储和传递信息,比如在多个请求之间传递数据。它可以让你在爬虫的不同阶段传递数据,通常用于保存跨请求的信息。
  • response.xpath():使用 XPath 表达式提取数据。返回的是一个 SelectorList 对象。
  • response.css():使用 CSS 选择器提取数据。它是另一种从 HTML 页面中提取数据的方式,功能与 xpath() 类似。
  • response.follow():用于追踪页面中的链接并发起新请求,通常用于爬取分页数据或者从页面中获取新的链接。
  • response.json():如果响应体是 JSON 格式的数据,可以通过该方法将响应体解析为 Python 字典。
  • response.encoding:响应内容的编码格式。如果 Content-Type 中包含了字符编码(如 charset=UTF-8),则会根据该编码来解析响应体。
  • response.request.url:请求地址。
  • response.request.headers:请求头。

image-20250131224632498

3.4数据解析

3.4.1response.xpath()

使用response响应对象中提供的xpath方法提取数据

使用response.xpath方法所获取的数据是类似列表的数据集SelectorList,其中包含的是selector对象。

    def parse(self, response):
        """
        :param response:
        :return:
        此函数为回调函数,对当前start_urls进行请求后,会将请求完成的响应对象传递给此函数
        response参数接收响应对象
        # """
        a_list = response.xpath('//div[@class="rank-list"]/a')
        # print(a_list)
        for a_temp in a_list:
            rank_number = a_temp.xpath('./div[@class="badge"]/text()')  # 排名
            img_url = a_temp.xpath('./img/@src')  # 图片地址
            title = a_temp.xpath('./div[@class="content"]/div[@class="title"]/text()')  # 标题
            desc = a_temp.xpath('./div[@class="content"]/div[@class="desc"]/text()')  # 描述
            play_number = a_temp.xpath('.//div[@class="info-item"][1]/span/text()')
            print('解析的数据:', rank_number, img_url, title, desc, play_number)

image-20250131225727432

3.4.2extract()

extract()方法:返回一个包含有字符串的列表

    def parse(self, response):
        """
        :param response:
        :return:
        此函数为回调函数,对当前start_urls进行请求后,会将请求完成的响应对象传递给此函数
        response参数接收响应对象
        """ 
        a_list = response.xpath('//div[@class="rank-list"]/a')
        # print(a_list)
        for a_temp in a_list:
            rank_number = a_temp.xpath("./div[@class='badge']//text()").extract()
            img_url = a_temp.xpath("./img/@src").extract()
            title = a_temp.xpath("./div[@class='content']/div[@class='title']/text()").extract()
            desc = a_temp.xpath("./div[@class='content']/div[@class='desc']/text()").extract()
            play_number = a_temp.xpath(".//div[@class='info-item'][1]/span/text()").extract()
            print('解析的数据:', rank_number, img_url, title, desc, play_number)

image-20250131225948245

3.4.3extract_first()

extract_first方法:返回列表中的第一个字符串,列表为空返回None

image-20250131230348590

4.管道

4.1相关概念

在 Scrapy 中,管道(Pipeline)是数据处理的关键组件之一。Scrapy 的管道机制允许你在爬虫抓取数据并进行解析后,进一步处理这些数据。管道通常用于存储、清理、过滤或者修改抓取的数据。

Scrapy 管道概述

管道是 Scrapy 用来处理爬虫返回数据的机制。每个管道类负责处理特定的数据操作,所有管道类是按顺序依次执行的。

  • 主要作用:管道用于处理 Scrapy 爬虫抓取的 Item 数据。
  • 工作流程:每个管道接收并处理爬虫返回的 Item 数据,然后将它传递给下一个管道。如果管道执行成功,Item 会继续向下传递;如果有任何错误,管道可能会丢弃这个 Item,或者采取其他处理措施。

Scrapy 管道的工作流程

  • Item 通过 Spider 返回:Spider 返回的 Item 会依次通过管道进行处理。

  • 管道执行顺序:Scrapy 会按照管道的顺序(在 settings.py 中定义的顺序)逐一调用每个管道进行数据处理。

  • 管道的处理:每个管道都会收到一个 Item,执行指定的操作,如数据清理、存储、过滤等。

  • 管道的返回值:

    • 如果管道成功处理了 Item,必须返回该 Item,表示继续将该数据传递给下一个管道。

    • 如果管道处理失败或者决定丢弃该 Item,可以返回 None

管道类的基本结构

一个简单的 Scrapy 管道类需要继承 scrapy.pipelines.Pipeline 类,并实现 process_item() 方法。以下是一个最基本的管道类结构:

process_item() 方法接收两个参数:

  • item:传递给管道的数据项,通常是一个字典或 scrapy.Item 对象。
  • spider:爬虫实例,允许你在管道中访问爬虫的一些属性。

image-20250131231525738

管道配置

管道的执行顺序可以在 settings.py 中通过 ITEM_PIPELINES 配置。每个管道类都有一个数字优先级,数字越小的管道优先执行。例如:

ITEM_PIPELINES = {
    'myproject.pipelines.MyPipeline': 1,
    'myproject.pipelines.AnotherPipeline': 2,
}

在上述配置中,MyPipeline 会在 AnotherPipeline 之前执行。管道的优先级顺序决定了处理顺序。数字越小的管道优先执行。

image-20250131231642265

管道的常见用途

  • 清理和过滤数据:对抓取的数据进行清理,去除无用信息,或者过滤掉不符合要求的内容。
  • 数据存储(数据库、文件等):将抓取的数据保存到数据库、文件、Excel 等存储介质中。
  • 去重处理:用于去重 Item,避免重复抓取和存储。
  • 文件下载和存储:如果抓取的数据是文件(如图片、PDF 文件等),可以使用管道来处理文件的下载和存储。

管道的高级功能

异步管道

如果需要在管道中执行网络请求或其他异步操作,Scrapy 提供了异步管道的支持。管道类可以通过 deferasync 来执行异步任务,并在处理完成后继续管道的执行。

from twisted.internet.defer import Deferred
from scrapy import Request

class AsyncPipeline:
    def process_item(self, item, spider):
        d = Deferred()
        spider.crawler.engine.downloader.download(Request(item['url']), spider)
        d.addCallback(self.handle_response, item)
        return d
    
    def handle_response(self, response, item):
        # 处理异步响应
        return item

管道返回 None 或抛出 DropItem

如果不希望继续处理某个 Item,可以返回 None,或者抛出 DropItem 异常。

from scrapy.exceptions import DropItem

class DropInvalidItemPipeline:
    def process_item(self, item, spider):
        if 'invalid' in item['title']:
            raise DropItem(f"Invalid item found: {item['title']}")
        return item

管道与爬虫的协作

  • 爬虫 (Spider):用于抓取数据。

  • 下载器中间件(Downloader Middleware):在请求和响应之间进行处理。

  • 管道(Pipeline):在抓取的数据返回给爬虫后处理它们。

Scrapy 的管道为你提供了一个强大的数据处理机制,能够高效地进行数据清理、存储、去重、过滤和转换等任务。

总结

Scrapy 的管道是一个用于处理抓取到的 Item 数据的机制,管道通过对数据的清理、存储、过滤等操作,使得抓取的过程更加灵活。你可以根据需要自定义管道类,并根据 ITEM_PIPELINES 配置它们的执行顺序。管道在数据抓取和处理过程中起到了关键作用,帮助你把抓取到的数据转化为最终的存储格式。

4.2yield

在 Scrapy 中,yield 用于返回数据(或请求)是因为 Scrapy 采用的是协程(generator)模型,它通过生成器的方式异步地进行请求和处理数据。

使用 yield 具有以下几个优势:

  • 提高性能和内存利用率yield 是 Python 中生成器的关键字,它会暂停当前函数的执行,并将控制权返回给调用者。通过生成器,你可以按需生成数据,而不是一次性将所有数据返回,避免了将大量数据一次性加载到内存中,从而提高了爬虫的性能和内存利用率。
  • 异步处理: Scrapy 使用 yield 来处理请求和数据的返回。每当一个请求返回时,Scrapy 会“暂停”当前的爬取操作,并立即继续爬取其他页面或处理其他任务。这种异步处理方式能有效提高爬虫的速度和响应效率。
  • 避免阻塞: 使用 yield 不会阻塞其他请求的执行,它让 Scrapy 能够并发地处理多个请求,避免了等待某个请求完成后才开始下一个请求的情况,从而加速了整个抓取过程。
  • 简化代码: 使用 yield 可以让 Scrapy 自动处理请求的返回和调度,而不需要手动管理请求队列、延迟等细节。Scrapy 会自动调度新的请求和数据,开发者只需要关注数据的提取和请求的生成。

代码

import scrapy

class MySpider(scrapy.Spider):
    name = 'my_spider'
    start_urls = ['http://example.com']

    def parse(self, response):
        # 提取数据并返回
        yield {
            'title': response.xpath('//title/text()').get(),
        }

        # 发起新的请求并继续处理
        next_page = response.xpath('//a[@class="next"]/@href').get()
        if next_page:
            yield response.follow(next_page, self.parse)

4.3管道的使用

在 Scrapy 中,Item Pipelines 是用来处理抓取到的 Item 数据的。你可以在 Item Pipeline 中执行诸如清洗数据、保存到数据库、导出文件等操作。你通过配置 ITEM_PIPELINES 来启用和配置不同的管道类。

  • ITEM_PIPELINES 是 Scrapy 设置项,用于指定哪些管道类(pipelines)将处理抓取到的 Item。

  • "fm.pipelines.FmPipeline" 是管道类的路径,表示该管道类位于 fm 项目的 pipelines.py 文件中。

  • 300 是优先级数字,Scrapy 会根据数字的大小来决定管道的执行顺序,数字越小的管道优先执行。

image-20250201152405396

解析完数据后,通过yield返回到下个管道处理数据保存

class FmPipeline:
    def process_item(self, item, spider):
        # print('pipline中的数据:', item)
        mongo_client = pymongo.MongoClient()
        collection = mongo_client['py_spider']['qingtingFM']
        collection.insert_one(item)
        print('数据插入成功:', item)
        # return item

image-20250201152314265

mongodb中数据已经保存完成

image-20250201152742994

5.Scrapy框架执行流程

相关概念

Scrapy Engine:这是 Scrapy 的核心,它负责协调整个爬虫流程,管理各个组件的协作。

Scheduler(调度器):调度器负责接收爬虫生成的请求(Requests),并按优先级将它们加入到队列中。请求按顺序被发送到下载器进行下载。

Downloader(下载器):下载器负责从互联网下载网页并返回响应。它会根据需要将请求传递给下载器中间件(Downloader Middlewares)进行进一步处理。

Requests 和 Responses:请求(Requests)是从爬虫生成的,响应(Responses)是下载器获取网页内容后返回的。这些请求和响应在整个过程中流动,传递数据。

Spiders(爬虫):爬虫是 Scrapy 框架的核心,负责从起始 URL 开始抓取页面,处理响应并提取数据(Items),然后生成新的请求。

Item Pipeline(数据管道):当数据(Items)被提取后,它会被传递到数据管道进行进一步处理,例如存储数据或清洗数据。

Middlewares(中间件):Scrapy 使用中间件来处理请求和响应的流程,例如处理请求的延迟、修改请求头、处理重定向等。

主要流程总结

  • 爬虫 提取数据并生成请求。
  • 请求通过 调度器 排队。
  • 请求被 下载器 发送到目标服务器。
  • 服务器返回 响应,并通过下载器传递回来。
  • 爬虫 解析响应并提取数据。
  • 数据通过 数据管道 进行处理。
  • 如有需要,爬虫会生成更多的请求,继续抓取。

image-20250201172353015

6.蜻蜓FM爬虫

6.1创建项目

命令

# 创建项目
scrapy startproject qtfmspider

# 打开项目目录
cd qtfmspider

# 创建爬虫脚本
scrapy genspider qingtingfm qingting.fm/rank

image-20250201173753230

6.2项目配置

6.2.1可访问域名

在 Scrapy 中,allowed_domains 是一个用于限制爬虫访问的域名列表。这个设置确保爬虫只会抓取来自指定域名的页面,避免爬取其他不相关的站点。

allowed_domains = ["qingting.fm", "pic.qtfm.cn"]

image-20250201225919003

这表示爬虫只会从 qingting.fmpic.qtfm.cn 这两个域名抓取数据。这样做有助于防止爬虫意外访问外部站点,并且能提高抓取的精确度。

注意事项:

  • qingting.fmpic.qtfm.cn 是完整的主域名。通常,allowed_domains 中的域名应该包括主域名,并且不包括 http://https://
  • pic.qtfm.cnqingting.fm 的子域名,所以可以同时添加这两个域名,确保能够抓取来自主域名和子域名的内容。
  • 如果你只想抓取 qingting.fm 这个站点的内容,而不希望爬取 pic.qtfm.cn 上的图片,建议只添加 qingting.fm,这样爬虫就不会请求 pic.qtfm.cn 上的资源。

这样配置之后,爬虫只会抓取 qingting.fmpic.qtfm.cn 的页面和资源,确保数据抓取的集中性和安全性。

我们需要访问图片,从而下载图片,所以需要配置图片的域名

image-20250201225740323

6.2.2数据管道

ITEM_PIPELINES 是 Scrapy 中用来指定和启用数据管道(Item Pipelines)的设置。 ITEM_PIPELINES 表示启用了 qtfmspider.pipelines.QtfmspiderPipeline 这个管道,并且给它指定了优先级 300

在 Scrapy 中,ITEM_PIPELINES 是一个字典,键是管道的路径,值是一个整数,表示管道的执行顺序。数字越小,优先级越高,越先执行。

ITEM_PIPELINES = {
   "qtfmspider.pipelines.QtfmspiderPipeline": 300,
}
  • qtfmspider.pipelines.QtfmspiderPipeline 是管道类的路径。管道类会在爬虫抓取完一项数据(Item)后处理数据。你需要确保 QtfmspiderPipeline 类存在于 pipelines.py 文件中。
  • 300 是管道的优先级。默认情况下,优先级是按照数字顺序执行的,数字小的先执行。

image-20250201230049505

6.2.3robots协议

地址:https://m.qingting.fm/robots.txt

蜻蜓FM的robots协议

  • User-agent: * 表示适用于所有爬虫

  • Disallow: /* 表示禁止访问所有包含问号(?)的 URL。

  • Disallow: /?* 也有类似的效果,禁止访问以问号开头的 URL。

  • Disallow: 后面没有指定路径时,表示允许访问所有页面。

image-20250201221813028

在 Scrapy 中,ROBOTSTXT_OBEY 是一个配置项,用来决定是否遵守网站的 robots.txt 文件的规则。如果将其设置为 False,Scrapy 就会忽略 robots.txt 中的限制,允许爬取任何页面。

# Obey robots.txt rules
ROBOTSTXT_OBEY = False
  • ROBOTSTXT_OBEY = False:表示 Scrapy 会忽略 robots.txt 文件中的规则,允许抓取网站上所有的页面,尽管 robots.txt 中可能会指定某些页面不允许抓取。

  • ROBOTSTXT_OBEY = True:表示 Scrapy 会遵守 robots.txt 文件中的规定,只有允许的页面会被爬取。

image-20250201230139493

不配置会出现以下错误

image-20250201175302218

6.2.4type字段

在 Scrapy 中,yield 用来生成一个 Item 或者一个字典(dict)来传递抓取到的数据给爬虫的管道(Item Pipeline)。通过 yield 语句,Scrapy 逐步处理并返回抓取到的结果,而不是将所有结果一次性返回。Scrapy 会根据这些 yield 返回的结果来执行后续的处理,比如数据存储、去重、或者进一步的抓取。

在 Scrapy 中,type 字段是一个自定义字段,用于标识当前 Item 的类型。它可以帮助你在数据处理和存储过程中进行分类和管理。通过 yield 语句,Scrapy 将抓取到的结果传递给后续的处理阶段(如 Item Pipeline),并通过 type 字段进行不同类型的处理。这种方式有助于在复杂爬虫中有效管理不同类型的数据。

save_img告诉管道此数据需要进行图片保存

save_info告诉管道此数据进行数据保存

image-20250201220145106

6.3代码

parse方法

 # 创建发送图片请求
            yield scrapy.Request(img_url, callback=self.parse_image, cb_kwargs={"img_name": title})

parse_image方法

 
    def parse_image(self, response, img_name):
        """
        解析图片
        :param response:
        :param image_name:
        :return:
        """
        # print('图片解析方法:', response.url)
        # print(image_name)
        # print(response.body)
        yield {
            'type': 'save_img',
            "img_name": img_name + ".png",
            "img_content": response.body
        }

process_item方法

class QtfmspiderPipeline:
    def process_item(self, item, spider):
        # 获取返回的数据类型
        type_ = item.get('type')
        if type_ == 'save_img':
            # getcwd(): 用于获取当前工作目录(Current Working Directory)的路径
            download_path = os.getcwd() + '/download/'
            if not os.path.exists(download_path):
                os.mkdir(download_path)
            # 图片保存
            img_name =  re.sub(r'[\\/*?:"<>|]', "", item.get("img_name"))
            image_content = item.get("img_content")
            with open(download_path + img_name, "wb") as f:
                f.write(image_content)
                print("图片保存成功: ", img_name)
        elif type_ == 'save_info':
            mongo_client = pymongo.MongoClient()
            collection = mongo_client['py_spider']['qingtingfm']
            collection.insert_one(item)
            print('数据插入成功:', item.get('title'))
        else:
            print('数据类型不符合规定...')

image-20250201220658452

7.豆瓣Top250爬虫

7.1创建项目

命令

# 创建项目
scrapy startproject douban

# 打开项目目录
cd douban

# 创建爬虫脚本
scrapy genspider doubantop250 "https://movie.douban.com/top250?start=0&filter="

image-20250201221342864

7.2项目配置

7.2.1robots协议

豆瓣robots协议地址:https://movie.douban.com/robots.txt

image-20250201222449337

配置豆瓣项目关闭rebots.txt验证。

image-20250201222609400

7.2.2配置User-Agent

有以下2中方式配置User-Agent

DEFAULT_REQUEST_HEADERS

Scrapy 中,DEFAULT_REQUEST_HEADERS 是一个配置项,用于定义默认的 HTTP 请求头。这些请求头会在你发起 HTTP 请求时自动附加到请求中,帮助你模拟浏览器行为,以便绕过一些简单的反爬机制(例如,检查 User-AgentReferer 等)。配置请求头可以让你在进行网络爬取时更加灵活和隐蔽。

# 也可以在headers中配置
# Override the default request headers:
DEFAULT_REQUEST_HEADERS = {
    # "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    # "Accept-Language": "en",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36"
}

User-Agent

Scrapy 中,User-Agent 配置是用来模拟浏览器或客户端发送 HTTP 请求时的一种方式。User-Agent 是 HTTP 请求头(headers)中的一个字段,用于告知服务器请求是来自哪种操作系统、浏览器或设备。配置这个字段在爬虫开发中非常重要,主要用于绕过反爬虫机制和模拟不同的客户端环境。

在配置文件中设置的请求头是固定的, 如果发送的请求过多也可能造成当前请求头失效。
所以需要在请求的过程中要对请求头进行随机变换,想要完成这种功能需要借助中间件完成。

# 配置 USER_AGENT
# USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36"

image-20250201223006879

7.2.4分页

检查页面上是否存在“下一页”的链接。如果存在,就抓取这个链接并递归地调用 parse 方法,以抓取下一页的数据。

  • 首先,代码通过 response.xpath("//span[@class='next']/a/@href") 获取“下一页”链接的相对 URL。如果该链接存在(即存在 .next 类的分页元素),说明还有下一页。
  • 使用 response.urljoin(next_url) 将相对 URL 转换为绝对 URL。
  • 然后,发起请求 scrapy.Request(url=next_url, callback=self.parse) 来抓取下一页的数据,并指定回调函数 parse 继续处理抓取到的页面。
  • 如果不存在下一页(即没有 .next 类的元素),表示分页已经结束,打印 爬取成功...

image-20250201232230684

7.2.3其他配置

这些配置上面都写过了,这里不详解了

添加图片的域名

image-20250201230309032

开启数据管道

image-20250201230411457

type参数

cb_kwargs的参数名向和回调函数callback的参数名一致

image-20250201231330381

7.3代码

Doubantop250Spider

import scrapy
from scrapy import cmdline


class Doubantop250Spider(scrapy.Spider):
    name = "doubantop250"
    allowed_domains = ["movie.douban.com", 'doubanio.com']
    start_urls = ["https://movie.douban.com/top250?start=0&filter="]

    def parse(self, response):
        li_list = response.xpath("//ol[@class='grid_view']/li")
        for li in li_list:
            img_url = li.xpath(".//img/@src").extract_first()
            title = li.xpath(".//span[@class='title'][1]/text()").extract_first()
            rating_num = li.xpath(".//span[@class='rating_num']/text()").extract_first()
            star = li.xpath(".//div[@class='star']/span[4]/text()").extract_first()
            # 数据保存
            yield {
                'type': 'save_info',
                'image_url': img_url,
                'title': title,
                'rating_num': rating_num,
                'star': star
            }
            # 请求图片链接,进行下载图片
            yield scrapy.Request(url=img_url, callback=self.parse_img, cb_kwargs={'img_name': title})
            # 翻页
            if response.xpath("//span[@class='next']/a/@href"):
                next_url = response.urljoin(response.xpath("//span[@class='next']/a/@href").extract_first())
                print('开始抓取下一页:', next_url)
                yield scrapy.Request(url=next_url, callback=self.parse)
            else:
                print('爬取成功...')

    @staticmethod
    def parse_img(response, img_name):
        yield {
            'type': 'save_img',
            'img_name': img_name + '.jpg',
            'img_content': response.body
        }


if __name__ == '__main__':
    cmdline.execute('scrapy crawl doubantop250'.split())

DoubanPipeline

# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html
import os

import pymongo
# useful for handling different item types with a single interface
from itemadapter import ItemAdapter


class DoubanPipeline:
    def process_item(self, item, spider):
        type_ = item.get('type')
        if type_ == 'save_info':
            mongo_client = pymongo.MongoClient()
            collection = mongo_client['py_spider']['doubantop250']
            collection.insert_one(item)
            print('添加成功:', item.get('title'))
        elif type_ == 'save_img':
            download_path = os.getcwd() + '/download/'
            if not os.path.exists(download_path):
                os.mkdir(download_path)

            img_name = item.get('img_name')
            image_content = item.get('img_content')
            with open(download_path + img_name, 'wb') as f:
                f.write(image_content)
                print('下载成功:', img_name)
        else:
            print(f'type参数有误:{type_}')
        return item

分页爬取了1次,IP就被封了

image-20250201232327797

8.Scrapy中间件

8.1概念

在 Scrapy 中,中间件(Middleware)是用于处理请求和响应的组件,可以在 Scrapy 的请求处理过程中进行各种操作,比如修改请求、响应,或者执行一些额外的逻辑。Scrapy 中间件分为两类:

  • 请求中间件(Request Middleware):处理发送到网站的请求。
  • 响应中间件(Response Middleware):处理从网站返回的响应。

Scrapy 中的中间件非常强大,允许你在请求和响应的过程中进行自定义操作。通过灵活配置中间件,你可以控制请求的重试、延迟、设置 headers、处理 cookies 等,增强爬虫的稳定性和功能。

根据Scrapy运行流程中所在位置不同分为:

  • 下载中间件

  • 爬虫中间件

中间件的工作流程

在 Scrapy 中,流程大致如下:

  • 请求首先通过 请求中间件,可以修改请求或执行一些操作(如设置 User-Agent,重试请求等)。
  • 请求会被发送到目标网站,等待响应。
  • 返回的响应会通过 响应中间件,可以修改响应内容或者执行一些额外的操作(如数据清洗,修改响应的编码等)。
  • 最终响应会传递给爬虫进行解析。

常见中间件

Scrapy 提供了几个内置的中间件,常用的包括:

  • UserAgentMiddleware:为每个请求添加一个 User-Agent
  • RetryMiddleware:如果请求失败(如超时、HTTP 错误等),则自动重试。
  • HttpCacheMiddleware:启用 HTTP 缓存,使得相同的请求在短时间内不会被重新发送。
  • RedirectMiddleware:处理 HTTP 重定向(3xx)。
  • CookiesMiddleware:处理请求中的 cookies。

但在Scrapy默认的情况下,两种中间件都在middlewares.py一个文件中。爬虫中间件使用方法和下载中间件相同,且功能重复,常使用下载中间件。

8.2process_request

在 Scrapy 中,process_request请求中间件 中的方法之一,它用于在请求发送之前进行处理。你可以在 process_request 方法中对请求对象 (request) 进行修改,例如设置请求头、代理、添加一些元数据,或者做一些额外的操作,比如日志记录。

process_request 方法的定义如下:

def process_request(self, request, spider):
    # 处理请求
    return None

参数解释:

  • request:当前正在处理的请求对象,包含了该请求的 URL、headers、cookies 等信息。
  • spider:当前爬虫对象,包含爬虫的配置信息。

返回值:

  • 如果中间件没有修改请求,返回 None,请求会继续传递到下一个中间件或者最终发送到目标网站。
  • 如果你希望中止请求流程(比如直接返回一个响应),可以在 process_request 方法中返回一个 Response 对象或者一个 Request 对象来替代原本的请求。这样会立刻停止对该请求的处理,跳过后续的中间件处理。

8.3process_response

在 Scrapy 中,process_response响应中间件 中的方法之一,它在响应返回到 Scrapy 框架时被调用,允许你对响应进行修改或者执行其他的操作,比如清洗数据、修改响应的内容、处理编码问题等。

process_response 方法的定义如下:

def process_response(self, request, response, spider):
    # 处理响应
    return response

参数解释:

  • request:返回响应的请求对象。这个请求对象包含了发送该请求时的 URL、headers、meta 数据等信息。
  • response:从目标网站返回的响应对象。通常是一个 scrapy.http.Response 实例,包含了网页内容、状态码等信息。
  • spider:当前爬虫对象,包含爬虫的配置信息。

返回值:

  • 修改响应:如果你希望对响应做一些修改,可以在 process_response 方法中对 response 进行修改,并返回修改后的 response
  • 中止响应处理:如果你希望中止后续的中间件处理(比如直接返回一个替代响应),可以返回一个 Response 对象,Scrapy 会停止后续的中间件处理。

8.4实现随机User-Agent

8.4.1创建项目

命令

# 创建项目
scrapy startproject middwaredemo

# 打开项目目录
cd middwaredemo1

# 创建爬虫脚本
scrapy genspider doubantop250 "https://movie.douban.com/top250?start=0&filter="

image-20250202163823200

8.4.2配置

配置下载中间件

image-20250202170823421

然后关闭robots

image-20250202171045260

暂时关闭分页,容易导致IP封禁

image-20250202171145781

8.4.3实现UserAgentDownloaderMiddleware

class UserAgentDownloaderMiddleware:
    USER_AGENTS_LIST = [
        "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 2.0.50727; Media Center PC 6.0)",
        "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 1.0.3705; .NET CLR 1.1.4322)",
        "Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.2; .NET CLR 1.1.4322; .NET CLR 2.0.50727; InfoPath.2; .NET CLR 3.0.04506.30)",
        "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.3 (Change: 287 c9dfb30)",
        "Mozilla/5.0 (X11; U; Linux; en-US) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.6",
        "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.2pre) Gecko/20070215 K-Ninja/2.1.1",
        "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9) Gecko/20080705 Firefox/3.0 Kapiko/3.0",
        "Mozilla/5.0 (X11; Linux i686; U;) Gecko/20070322 Kazehakase/0.4.5"
    ]

    def process_request(self, request, spider):
        print("------UA中间件----")
        # 随机选择UA
        user_agent = random.choice(self.USER_AGENTS_LIST)
        request.headers['User-Agent'] = user_agent
        # 不写return
        """
        如果返回None, 表示当前的response提交下一个权重低的process_request。
        如果传递到最后一个process_request,则传递给下载器进行下载。
        """

image-20250202171259283

8.5DOWNLOAD_DELAY(请求延迟)

地址:https://docs.scrapy.net.cn/en/latest/topics/settings.html#std-setting-DOWNLOAD_DELAY

image-20250202172356688

在 Scrapy 中,DOWNLOAD_DELAY 是一个非常重要的配置项,它用于控制请求之间的间隔时间(即每个请求发送之间的延迟)。设置这个参数可以帮助你遵守目标网站的抓取规则,减少因频繁请求而导致被封锁的风险,同时也有助于减轻目标网站的负载。

DOWNLOAD_DELAY 用来指定 Scrapy 在两个连续请求之间的最小延迟时间(单位:秒)。它可以帮助你调整爬虫的请求频率,从而避免目标网站的反爬虫机制(如频繁请求导致 IP 被封禁)。

默认值

默认情况下,DOWNLOAD_DELAY0,即没有延迟,Scrapy 会尽可能地快速发送请求。

语法

DOWNLOAD_DELAY = 0

工作原理

当 Scrapy 发送请求时,它会在每个请求之间插入一个延迟时间。具体延迟的时间是由 DOWNLOAD_DELAY 设置的值决定的。该值是爬虫每发送一个请求后暂停的时间,防止目标网站被过度负载。

例如:

  • 如果 DOWNLOAD_DELAY = 1.0,那么每个请求之间会有 1 秒的延迟。
  • 如果 DOWNLOAD_DELAY = 2.5,那么每个请求之间会有 2.5 秒的延迟。

image-20250202172510150

8.6start_requests(分页)

之前分页我们使用urljoin拼接分页链接进行请求,因为该网站是个静态网站,我们可以获取页面链接。

对于动态网站,我们则需要通过请求api的方式请求分页,这里就是用到了start_requests

image-20250202173220882

在 Scrapy 中,start_requests 方法是爬虫开始抓取的入口点之一。它是 Scrapy 爬虫类中的一个默认方法,可以用来生成初始请求并开始爬取数据。通常,start_requests 方法用来生成爬虫启动时需要抓取的第一个请求列表,之后 Scrapy 会根据这些请求开始抓取网页。

start_requests 是一个生成器函数,用来返回初始的 Request 对象。在 Scrapy 中,yield 语句用于返回一个请求 (Request),它会将该请求交给 Scrapy 引擎处理,然后继续执行后面的代码。当 Scrapy 处理完当前请求之后,会继续调用该方法生成下一个请求。

简单理解,就是生成所有的分页链接,然后通过 scrapy.Request进行请求

yield 语句会返回一个 scrapy.Request 对象,它表示一个 HTTP 请求,Scrapy 引擎会将这个请求加入到调度队列中,然后发起请求。scrapy.Request(url) 会将构建好的 URL 作为请求发送到目标网站。

Scrapy 会通过中间件处理这个请求,并在请求响应回来后,调用爬虫类中定义的回调方法(通常是 parse 方法),然后继续抓取数据。

    def start_requests(self):
        for page in range(0, 3):
            url = f'https://movie.douban.com/top250?start={page * 25}&filter='
            yield scrapy.Request(url)

这里记得修改DOWNLOAD_DELAY=3,我这里保险起见只爬取3页,防止IP封禁

还有这几个配置

image-20250202174138296

爬取完成

image-20250202173959238

8.6设置IP代理

8.6.1免费代理

    def process_request(self, request, spider):
        print("------UA中间件------")
        # 随机选择UA
        user_agent = random.choice(self.USER_AGENTS_LIST)
        request.headers['User-Agent'] = user_agent
        # 设置免费代理: 有些代理在发送请求时会失败
        request.meta['proxy'] = 'http://127.0.0.1:10809'
        # print(request.meta)
        return None
        # 不写return
        """
        如果返回None, 表示当前的response提交下一个权重低的process_request。
        如果传递到最后一个process_request,则传递给下载器进行下载。
        """

    def process_response(self, request, response, spider):
        print('------process_response------')
        # response.status = 302
        if response.status != 200:
            # raise TimeoutError
            request.dont_filter = True  # 关闭过滤, 可以发送之前发送过的请求
            return request
        return response

8.6.2付费代理

快代理文档:https://www.kuaidaili.com/doc/dev/sdk_http/#proxy_python-scrapy

大致也是通过中间件实现

image-20250202174747816

8.7在Scrapy中使用Selenium

爬取腾讯招聘网站:https://careers.tencent.com/search.html

image-20250204203453476

8.7.1创建项目

命令

# 创建项目
scrapy startproject TxJob
# 打开项目目录
cd TxJob
# 创建爬虫脚本
scrapy genspider TxJobSpider https://careers.tencent.com/search.html

image-20250204203908080

8.7.2分析页面

地址:https://careers.tencent.com/search.html?index=2

分页会根据地址栏参数index获取每页数据

页面比较简单,之前也爬取过,获取每个职位的div,然后再获响应的数据即可

//div[@class='correlation-degree']//div[@class='recruit-list']

image-20250204210106966

8.7.3Spider

爬虫模块:

  • 获取要爬取的分页地址
  • 解析页面
  • TxjobPipeline管道获取每个职位信息后进行打印
    def start_requests(self):
        url = 'https://careers.tencent.com/search.html?index={}'
        # 方便测试 只爬3页
        for page in range(1, 4):
            yield Request(url=url.format(page))

    def parse(self, response):
        div_list = response.xpath("//div[@class='correlation-degree']//div[@class='recruit-list']")
        for div in div_list:
            item = dict()
            item['title'] = div.xpath('./a//span[@class="job-recruit-title"]/text()').extract_first()
            item['department'] = div.xpath('./a/p[1]/span[1]/text()').extract_first()
            item['address'] = div.xpath('./a//span[2]/text()').extract_first()
            item['post'] = div.xpath('./a/p[1]/span[3]/text()').extract_first()
            item['date'] = div.xpath('./a/p[1]/span[last()]/text()').extract_first()
            item['recruit_data'] = div.xpath('./a/p[2]/text()').extract_first()
            yield item

if __name__ == '__main__':
    cmdline.execute('scrapy crawl TxJobSpider'.split())

image-20250204212923951

8.7.4Selenium

8.7.4.1使用Selenium的方式

Scrapy Spider 中使用 Selenium 和在 Middleware 中使用 Selenium 各有优缺点,主要的区别在于 灵活性代码结构。下面我们详细对比它们的区别,帮助你选择合适的方式。

方式 适用场景 优点 缺点
在 Spider 里用 Selenium 只需部分页面使用 Selenium,或者需要模拟用户交互(如点击、滚动、输入) 灵活、可控,适合复杂页面 代码耦合,影响 Scrapy 性能
在 Middleware 里用 Selenium 所有页面都依赖 Selenium 渲染 代码整洁,多个 Spider 共享 WebDriver 影响 Scrapy 并发,无法灵活控制 Selenium 交互

总结

  • 如果只有少量页面需要 Selenium,在 Spider 里用 Selenium,更灵活,性能更高。
  • 如果所有页面都要 Selenium 解析,可以在 Middleware 里用 Selenium,代码清晰,但性能较低。

我们这里是静态网站,所以需要在Middleware 里用 Selenium

8.7.4.2安装scrapy-selenium

首先需要安装scrapy-selenium

pip install scrapy-selenium

image-20250204212655298

8.7.4.3代码

主要用于:

  • 处理传递给 SpiderResponse
  • 处理 Spider 生成的 RequestItem

webdriver.Chrome():在 __init__ 方法中,创建了Chrome WebDriver实例。

from_crawler 是 Scrapy 组件生命周期管理的工厂方法,用于实例化 SeleniumDownloaderMiddleware 中间件。Scrapy 启动爬虫时,会调用 from_crawler 方法创建中间件实例 s

  • 它允许访问 Scrapy 的 crawler 对象
  • 获取全局配置(settings),如果需要读取 Scrapy 配置项,可以通过 crawler.settings 访问。
  • 绑定 Scrapy 的信号(signals),在爬虫关闭时执行 spider_closed 方法,防止 WebDriver 进程残留。

crawler.signals.connect(s.spider_closed, signal=signals.spider_closed)

  • 监听 Scrapy 的 spider_closed 信号,当爬虫关闭时,调用 spider_closed 方法来关闭 WebDriver,防止资源泄露。

process_request 是 Scrapy 下载中间件的核心方法,用于拦截请求,并让 Selenium 加载网页、执行 JavaScript,然后返回 HTML 内容。

class SeleniumDownloaderMiddleware:
    def __init__(self):
        self.browser = webdriver.Chrome()

    # 检测爬虫状态
    @classmethod
    def from_crawler(cls, crawler):
        s = cls()
        crawler.signals.connect(s.spider_closed, signal=signals.spider_closed)
        return s

    def process_request(self, request, spider):
        self.browser.get(request.url)
        wait = WebDriverWait(self.browser, 10)
        wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '.recruit-list')))
        body = self.browser.page_source
        return HtmlResponse(url=request.url, body=body, encoding='utf-8')

    def spider_closed(self):
        self.browser.quit()

配置settings.py

ROBOTSTXT_OBEY = False # 关闭robots一些
DOWNLOAD_DELAY = 1     # 请求延时1s 防止ip封禁
# 下载中间件 使用selenium功能
DOWNLOADER_MIDDLEWARES = {
   # "TxJob.middlewares.TxjobDownloaderMiddleware": 543,
   "TxJob.middlewares.SeleniumDownloaderMiddleware": 543
}
# 开启管道 用于处理数据
ITEM_PIPELINES = {
   "TxJob.pipelines.TxjobPipeline": 300,
}

image-20250204215322679

启动爬虫,爬取了3页,每页10条

image-20250204215535365

9.多管道使用

Scrapy 允许使用多个管道(Pipelines)来处理不同的任务。多个管道可以按照特定的顺序处理数据,每个管道都能对数据进行独立的处理或修改。

流程:

  • 数据从爬虫传递到管道:每个 item 被爬虫返回后会被传递到管道进行处理。
  • 多个管道执行:Scrapy 按照管道的顺序处理数据,直到所有的管道都执行完毕。
  • 管道的返回值
    • 如果管道返回 None,Scrapy 会跳过当前管道,进入下一个管道。
    • 如果管道返回修改过的 item,Scrapy 会继续传递该 item 到下一个管道。
  • 配置多个管道:在 settings.py 中,配置 ITEM_PIPELINES 字典,其中管道的值是管道的类路径,优先级是一个整数,数值越小,优先级越高。Scrapy 会按照优先级的顺序执行管道。
ITEM_PIPELINES = {
    'myproject.pipelines.PipelineOne': 1,  # 优先级 1
    'myproject.pipelines.PipelineTwo': 2,  # 优先级 2
    'myproject.pipelines.PipelineThree': 3,  # 优先级 3
}

创建TxJobFilePipelineTxJobMongoDBPipeline用于将数据保存文件和保存到MongoDB

根据settings.py配置先执行TxJobFilePipeline在执行TxJobMongoDBPipeline

open_spider 方法是 Scrapy 管道(Pipeline)中的一个钩子方法,在爬虫启动时只执行 一次,用于进行一些初始化操作。


class TxJobFilePipeline:
    """
    保存json文件
    """

    def open_spider(self, spider):
        # 爬虫开启时仅运行一次
        if spider.name == 'TxJobSpider':
            self.file_obj = open('tx_json_data.txt', 'a', encoding='utf-8')

    def process_item(self, item, spider):
        if spider.name == 'TxJobSpider':
            print('TxJobFilePipeline被执行...')
            self.file_obj.write(json.dumps(item, ensure_ascii=False, indent=4) + '\n')
            return item  # 将数据写入完成之后把原数据传递给下一个pipeline

    def close_spider(self, spider):
        # 爬虫关闭时仅运行一次
        if spider.name == 'TxJobSpider':
            self.file_obj.close()


class TxJobMongoDBPipeline:
    """
    保存MongoDB
    """

    def open_spider(self, spider):
        # 爬虫开启时仅运行一次
        if spider.name == 'TxJobSpider':
            print('open_spider被执行...')
            self.mongo_client = pymongo.MongoClient()
            self.collection = self.mongo_client['py_spider']['tx_job_info']

    def process_item(self, item, spider):
        if spider.name == 'TxJobSpider':
            print('TxJobMongoDBPipeline被执行...')
            self.collection.insert_one(item)
            print('数据插入成功:', item)
            # 没有管道了 可以不用return
            # return item  # 将数据写入完成之后把原数据传递给下一个pipeline

    def close_spider(self, spider):
        # 爬虫关闭时仅运行一次
        if spider.name == 'TxJobSpider':
            print('close_spider被执行...')
            self.mongo_client.close()

image-20250204221029821

文件和Mongodb都保存成功

image-20250204221209554

10.数据去重

10.1数据去重

该管道的作用是去重,防止爬取的重复数据进入数据库或文件存储。它使用 Redis + MD5 哈希 来进行数据去重。

  • 使用 Redis 作为存储介质

  • 对 Item 进行 MD5 哈希生成唯一标识

  • 利用 Redis 存储已处理的 Item 哈希值 进行去重

class TxJobCheckPipeline:
    def open_spider(self, spider):
        if spider.name == 'TxJobSpider':
            self.redis_client = redis.Redis()

    def process_item(self, item, spider):
        if spider.name == 'TxJobSpider':
            # 将传递过来的item数据转为字符串并加密成md5数据
            item_str = json.dumps(item, ensure_ascii=False, indent=4)
            md5_hash = hashlib.md5()
            md5_hash.update(item_str.encode('utf-8'))
            hash_value = md5_hash.hexdigest()

            # 判断hash值是否存在于redis中
            if self.redis_client.get(f'tx_job_item_filter:{hash_value}'):
                # 如果存在则抛出异常停止管道传递数据
                raise DropItem('数据已存在...')
            else:
                # 如果不存在则将hash保存到redis中
                # tx_work_filter:前缀会在redis中创建文件夹, 便于管理
                self.redis_client.set(f'tx_job_item_filter:{hash_value}', item_str)
            return item

    def close_spider(self, spider):
        if spider.name == 'TxJobSpider':
            self.redis_client.close()

image-20250204234902274

10.2地址去重

通过生成 URL 的 MD5 值并使用 Redis 来记录已经访问过的 URL,避免对相同页面进行重复抓取。

  • 生成分页 URL;

  • 使用 MD5 对每个 URL 进行哈希;

  • 检查 URL 是否已经抓取过(通过 Redis 存储哈希值);

  • 如果未抓取过,则发起请求,并将 URL 的哈希值存入 Redis。

    def start_requests(self):
        url = 'https://careers.tencent.com/search.html?index={}'
        # 方便测试 只爬3页
        for page in range(1, 4):
            md5_hash = hashlib.md5()
            md5_hash.update(url.format(page).encode('utf-8'))
            url_md5_value = md5_hash.hexdigest()
            if self.redis_client.get(f'txjob_url_filter:{url_md5_value}'):
                print(f"地址重复:{url.format(page)}")
                continue
            else:
                self.redis_client.set(f'txjob_url_filter:{url_md5_value}', url.format(page))
            yield Request(url=url.format(page))

image-20250204234954579

11.增量爬虫

这条命令启动名为 MySpider 的爬虫,并设置爬虫状态存储目录为 crawls/my_spider-1。如果爬虫中途因故停止,它可以从该目录中的状态信息继续爬取,不必重新开始爬取。

  • scrapy crawl 是 Scrapy 的命令行接口中的一个指令,用于启动一个爬虫并执行抓取任务。

  • MySpider 爬虫的名称,Scrapy 会根据该名称找到相应的爬虫类并开始爬取。

  • -s 是 Scrapy 中的一个参数,用于设置自定义配置。

  • JOBDIR 是 Scrapy 的一个配置项,它指定了爬虫的 "job directory"(作业目录),用于存储爬虫的状态信息。

    • Scrapy 的爬虫会在每次执行时,保存抓取过程中的一些信息,例如已经爬取过的 URL 列表、请求的状态等。这样,如果爬虫由于某些原因中断(比如机器重启、网络问题等),它可以恢复之前的工作,而不是从头开始抓取所有数据。
    • JOBDIR=crawls/my_spider-1 指定了存储状态信息的目录路径,在本例中,该目录是 crawls/my_spider-1。目录 crawls/my_spider-1 中会存储该爬虫运行时的状态信息。
    • 这个目录的名字 my_spider-1 可以帮助区分不同的爬虫任务,避免不同爬虫的状态信息混在一起。如果需要恢复爬虫,可以指定相应的 JOBDIR 目录来加载爬虫的状态。
# 例子:scrapy crawl TxJobSpider -s JOBDIR=crawls/TxJobSpider-1
scrapy crawl MySpider -s JOBDIR=crawls/my_spider-1

第一次使用会启动爬虫

image-20250204235753238

在终端启动爬虫之后,只需要按下ctrl + c就可以让爬虫暂停

注意点:ctrl + c不能执行两次,只需一次即可

image-20250204235915036

再次执行,爬虫会继续运行

启动爬虫和回复爬虫指令一样

scrapy crawl TxJobSpider -s JOBDIR=crawls/TxJobSpider-1

image-20250205000218007

12.dont_filter

在 Scrapy 中,dont_filter 是一个用于控制请求去重机制的参数。默认情况下,Scrapy 会自动去重请求,以避免重复抓取相同的页面。dont_filter 允许你在特定情况下跳过请求去重检查,强制爬虫发出请求,即使该请求已经存在于去重队列中。

默认请求去重机制

Scrapy 在发出请求时会使用去重机制(通过请求的 urlmeta 信息)来防止重复请求。如果 Scrapy 检测到已经请求过某个 URL,它会跳过该请求并不会再次发送。

dont_filter参数

  • dont_filter 是 Scrapy 中 Request 对象的一个布尔值参数。当你发出请求时,如果将 dont_filter=True,Scrapy 就不会对该请求进行去重处理,确保该请求被执行。
  • 如果 dont_filter=False(默认值),Scrapy 会对该请求进行去重检查,防止重复请求。
  • 如果 dont_filter 设置为 True,该请求将被强制发出,即使它之前已经被访问过。

使用场景:强制重新抓取相同页面、避免去重对特定请求的影响等。

scrapy.Request初始化方法部分源码:

image-20250205000645628

13.start_requests

start_requests 是 Scrapy 爬虫中一个重要的方法,它在爬虫启动时被调用,用于生成并发起初始请求。Scrapy 爬虫通常会在 start_requests 方法中定义抓取的起始页面(或一系列页面),然后根据这些起始请求来进行后续的页面抓取。

start_requests 方法的主要作用是生成爬虫开始抓取的初始请求。默认情况下,Scrapy 会使用爬虫类中的 start_urls 属性作为初始请求的 URL 列表,但是你也可以自定义 start_requests 方法以便更灵活地控制请求的生成和处理。

start_requests 的工作原理

  • Scrapy 启动时调用:当你运行 scrapy crawl <spider_name> 命令时,Scrapy 会在爬虫启动时调用 start_requests 方法,生成并发起初始请求。
  • 返回 Request 对象start_requests 方法需要返回一个或多个 Request 对象,Scrapy 会调度并发送这些请求。
  • 请求的回调:每个请求都应该有一个回调方法,通常是 parse,Scrapy 会在请求完成时调用该方法来处理响应。

14.Post请求

目标站点:http://www.cninfo.com.cn/new/commonUrl?url=disclosure/list/notice#szse

image-20250205002030918

14.1创建项目

命令

# 创建项目
scrapy startproject CNInfo
# 打开项目目录
cd CNInfo
# 创建爬虫脚本
scrapy genspider CNInfoSS http://www.cninfo.com.cn/new/disclosure

image-20250205002447947

14.2设置源代码根目录

将项目目录设置为源代码根目录

image-20250205011648702

要不然相对路径导入包的时候会报错

image-20250205012044302

image-20250205012250590

14.3Scrapy.Item

在 Scrapy 中,scrapy.Item 是用于定义爬虫抓取的数据结构,它相当于一个容器,用来存储爬虫在抓取过程中的数据。通过 scrapy.Item 定义的字段是我们在爬虫中提取和存储的数据。具体来说,scrapy.Field() 是用来定义每个字段的数据类型,它是一个简单的容器,存储着爬虫抓取的数据。

scrapy.Item:

  • scrapy.Item 是 Scrapy 中用于存储抓取到的数据的容器类。你可以定义一个类继承自 scrapy.Item,然后通过 scrapy.Field() 来为该类定义具体的数据字段。
  • 每一个继承自 scrapy.Item 的类都是一个数据结构,用于存储不同字段的数据。
  • Scrapy 会在爬取的过程中自动创建该类的实例,并通过各个字段来存储抓取到的数据。

scrapy.Field:

  • scrapy.Field() 是用来定义每个数据字段的,实际上它是一个容器,它允许 Scrapy 存储和操作该字段的数据。
  • scrapy.Field() 没有特别的功能,它只是一个字典类型的占位符,用来给 Item 类中的每个字段定义名称。
  • Field 本身是一个容器,并不存储数据,而是让 Scrapy 在抓取到数据后存放在这个字段里。

14.4Request

Scrapy 提供了多种请求类型来适应不同的抓取需求,常见的有:

  1. Request: 基本的 HTTP 请求,用于普通的抓取操作。
  2. FormRequest: 用于表单提交,适合登录、搜索等操作。
  3. JsonRequest: 用于发送 JSON 数据请求,常见于与 API 交互。
  4. XMLRequest: 用于发送 XML 数据请求,适合与 XML 格式的数据交互。
  5. SeleniumRequest: 用于抓取动态内容,结合 Selenium 使用。
  6. CurlRequest: 用于执行 curl 命令,适用于复杂请求场景。
  7. RedirectRequest: 用于处理 HTTP 重定向,自动跟踪重定向链接。

14.5代码

CNInfoItem

class CNInfoItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    announcementTitle = scrapy.Field()
    announcementTypeName = scrapy.Field()
    batchNum = scrapy.Field()
    secName = scrapy.Field()
    adjunctType = scrapy.Field()

CninfossSpider

import json
import scrapy
from scrapy import cmdline
from ..items import CNInfoItem
# from CNInfo.items import CNInfoItem


class CninfossSpider(scrapy.Spider):
    name = "CNInfoSS"
    allowed_domains = ["www.cninfo.com.cn"]
    start_urls = ["http://www.cninfo.com.cn/new/disclosure"]

    def start_requests(self):
        url = 'http://www.cninfo.com.cn/new/disclosure'

        # 表单数据
        for page in range(1, 3):
            data = {
                'column': 'szse_latest',
                'pageNum': str(page),
                'pageSize': '30',
                'sortName': '',
                'sortType': '',
                'clusterFlag': 'true',
            }

            yield scrapy.FormRequest(url=url, formdata=data, callback=self.parse, dont_filter=False)
            # yield scrapy.Request(url=url, body=json.dumps(data), method='POST', callback=self.parse)

    def parse(self, response, **kwargs):
        for info_list in response.json()['classifiedAnnouncements']:
            for info in info_list:
                item = CNInfoItem()
                item['announcementTitle'] = info['announcementTitle']
                item['announcementTypeName'] = info['announcementTypeName']
                item['batchNum'] = info['batchNum']
                item['secName'] = info['secName']
                item['adjunctType'] = info['adjunctType']
                yield item


if __name__ == '__main__':
    cmdline.execute('scrapy crawl CNInfoSS'.split())

CNInfoMongoPipeline

class CNInfoMongoPipeline:
    def open_spider(self, spider):
        if spider.name == 'CNInfoSS':
            self.mongo_client = pymongo.MongoClient()
            self.collection = self.mongo_client['py_spider']['cn_info']

    def process_item(self, item, spider):
        if spider.name == 'CNInfoSS':
            self.collection.insert_one(dict(item))
        return item

    def close_spider(self, spider):
        if spider.name == 'CNInfoSS':
            self.mongo_client.close()

MongoDB

image-20250205013352739

📌 创作不易,感谢支持!
每一篇内容都凝聚了心血与热情,如果我的内容对您有帮助,欢迎请我喝杯咖啡☕,您的支持是我持续分享的最大动力!

wxzf
posted @ 2025-04-08 22:20  peng_boke  阅读(451)  评论(0)    收藏  举报