第-7章-Python-爬虫框架-Scrapy(上)

第 7章 Python 爬虫框架 Scrapy(上)

编写爬虫可以看成行军打仗,基本的角色有两个:士兵和将军,士兵冲锋陷阵,而将军更多地是调兵遣将。框架就像一个将军,里面包含了爬虫的全部流程、异常处理和任务调度等。除了可以让我们少写一些烦琐的代码,学习框架还可以学到编程思想和提升编程能力。Python 中比较流行的爬虫框架有Pyspider 和 Scrapy

7.1 Scrapy 框架简介与安装

Scrapy,是用 Python 语言开发的一个快速、高层次的屏幕/Web 抓取框架,用于抓取 Web 站点并从页面中提取结构化数据。
Scrapy 使用 Twisted 异步网络请求框架来处理网络通信,不需要额外实现异步框架,而且包含各种中间件接口,能灵活地实现各种需求。Scrapy 的用途广泛,常用于数据挖掘、监测和自动化测试。
7.1.1 Scrapy 相关信息

  • 官网:https://scrapy.org/
  • 官方文档:https://doc.scrapy.org/en/latest/
  • 中文文档:https://scrapy-chs.readthedocs.io

7.1.2 Scrapy 的安装

Windows 环境和 Ubuntu 环境下可以直接通过 pip 命令安装,或者通过 Anaconda 安装,命令如下:

# 使用pip君令安装Scrapy
pip install Scrapy
# pip安装PyWin32
pip install pywin32
# 使用Anaconda安装Scrapy:
conda install Scrapy

在执行 pip 命令安装时可能会报错,出现如下错误信息(滚动底部):

error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Build Tools": 
http://landinghub.visualstudio.com/visual-cpp-build-tools

只需要访问 https://www.lfd.uci.edu/~gohlke/pythonlibs/ ,找到 Twisted,然后下载对应的版本即可。
在命令行输入 Python,查看自己的计算机对应的版本:

λ python
Python 3.7.0 (v3.7.0:1bf9cc5093, Jun 27 2018, 04:06:47) [MSC v.1914 32 bit (Intel)] on 
win64
Type "help", "copyright", "credits" or "license" for more information.

安装完 Scrapy,可以在终端输入 scrapy,以此验证 Scrapy 是否安装成功,出现如下信息代表安装成功:

 scrapy
Scrapy 1.5.1 - no active project
Usage:
 scrapy <command> [options] [args]
Available commands:
 bench Run quick benchmark test
 fetch Fetch a URL using the Scrapy downloader
 genspider Generate new spider using pre-defined templates
 runspider Run a self-contained spider (without creating a project)
 settings Get settings values
 shell Interactive scraping console
 startproject Create new project
 version Print Scrapy version
 view Open URL in browser, as seen by Scrapy
 [ more ] More commands available when run from project directory
Use "scrapy <command> -h" to see more info about a command

7.2 实战:爬取某网站每日壁纸

单击右下角的左右箭头可以切换壁纸,页面没有刷新,从前端网页无法得到全部的图片数据。F12 键打开 Chrome 抓包,过滤 XHR。通过搜索图片的名称。
image
将url中的网址复制搜索可以看到图片。
分析完请求链接和相应结果,我们要做的就是使用 Scrapy 模拟请求这个链接,解析JSON 来获得图片 URL。

7.2.2 创建爬虫脚本

逻辑弄清楚了,接下来新建一个 Scrapy 项目进行爬虫的辨析。Scrapy 创建项目需要通过命令行,这里我们新建一个爬虫项目bing,命令如下:

scrapy startproject bing

执行后,输出创建成功的信息:

New Scrapy project 'bing', using template directory 
'c:x/xx/xpython\\python37-32\\lib\\site-packages\\scra
py\\templates\\project', created in:
 E:\Code\Python\bing
You can start your first spider with:
 cd bing
 scrapy genspider example example.com

在命令行输入命令 tree /f,可以自动生成项目结构,如下所示:

E:. 
│ scrapy.cfg # 项目的配置文件 
└─bing # 项目的Python模块,会从这里引用代码 
 │ items.py # 项目的目标文件 
 │ middlewares.p # 中间件文件
 │ pipelines.py # 项目的管道文件
 │ settings.py # 项目的设置文件
 │ __init__.py 
 ├─spiders # 存储爬虫代码目录
 │ │ __init__.py
 │ │ 
 │ └─__pycache__
 └─__pycache__

每个文件都有具体的作用,我们先执行下述命令生成一个爬虫,而不用自己手写:

scrapy genspider BingWallpaper "cn.bing.com"

运行后,控制台输出成功创建的信息:

Created spider 'BingWallpaper' using template 'basic' in module:
 bing.spiders.BingWallpaper

可以看到在 spiders 下生成了一个文件 BingSpider.py,里面定义了lBingWallpaperSpider 类,内容如下:

# -*- coding: utf-8 -*-
import scrapy
class BingWallpaperSpider(scrapy.Spider):
 name = 'BingWallpaper'
 allowed_domains = ['cn.bing.com']
 start_urls = ['http://cn.bing.com/']
 def parse(self, response):
 pass

从上面的代码,我们知道生成的BingWallpaperSpider 类继承了scrapy.Spider 类,默认实现下述三个属性和一个函数。

  • name:爬虫的识别名称必须唯一,每个爬虫必须定义不同的名字。
  • allowed_domains:搜索的域名范围,或者说是爬虫的约束范围,只爬取该域名下的网页,不存在的 URL 会被忽略。
  • start_urls:爬取的 URL 元组,爬虫会从这里开始抓取数据。
  • parse:解析函数。默认情况下,初始 URL 请求完成下载后执行,参数是每个 URL传回的 Response 对象,主要作用是解析返回的网页数据(response.body)、提取结构化数据(生成 item)和生成下一页 URL 请求。

7.2.3 编写爬虫脚本

我们修改上述代码,模拟请求。

import scrapy
import json
import re
import os  # 添加这一行

class BingwallpaperSpider(scrapy.Spider):
    name = "BingWallpaper"
    allowed_domains = ["cn.bing.com", "cn.bing.net", "s.cn.bing.net"]
    start_urls = ["https://cn.bing.com/hp/api/model"]
    ROBOTSTXT_OBEY = False

    def parse(self, response):
        # 将响应内容解析为 JSON 格式
        data_json = json.loads(response.body)
        image_urls = []
        for img in data_json['MediaContents']:
            img = img['ImageContent']['Image']['Url']
            img_url = img.split('th')[1]
            filename = img_url.split('.')[1]
            content = 'https://s.cn.bing.net/th' + img_url
            # 清除文件名中的非法字符
            filename = re.sub('[^0-9a-zA-Z]+', '_', filename)
            image_urls.append((content, filename))

        for url, filename in image_urls:
            yield scrapy.Request(url, callback=self.save, meta={'filename': filename})

    def save(self, response):
        # 从meta中获取文件名
        filename = response.meta['filename']
        # 创建一个新的文件夹
        folder_name = 'bing_wallpapers'
        os.makedirs(folder_name, exist_ok=True)
        # 构建文件路径
        file_path = os.path.join(folder_name, filename)
        with open(f'{file_path}.jpg', 'wb') as f:
            f.write(response.body)
        self.log(f'Saved file {filename} in {folder_name}')

7.2.4 运行爬虫脚本

编写完成,准备运行这个脚本,但是我们发现 PyCharm 中运行的按钮是灰色的,需要在命令行中执行下述命令来运行爬虫:

scrapy crawl BingWallpaper

爬虫运行后,在控制台打印部分输出结果如下:

2024-02-10 15:29:59 [scrapy.utils.log] INFO: Scrapy 2.10.0 started (bot: bing)
2024-02-10 15:29:59 [scrapy.utils.log] INFO: Versions: lxml 4.9.3.0, libxml2 2.10.3, cssselect 1.2.0, parsel 1.8.1, w3lib 2.1.1, Twisted 22.10.0, Python 3.8.8 (tags/v3.8.8:024d805, Feb
 19 2021, 13:18:16) [MSC v.1928 64 bit (AMD64)], pyOpenSSL 23.2.0 (OpenSSL 3.1.2 1 Aug 2023), cryptography 41.0.3, Platform Windows-10-10.0.19041-SP0
2024-02-10 15:29:59 [scrapy.addons] INFO: Enabled addons:
[]
2024-02-10 15:29:59 [scrapy.crawler] INFO: Overridden settings:
{'BOT_NAME': 'bing',
 'FEED_EXPORT_ENCODING': 'utf-8',
 'NEWSPIDER_MODULE': 'bing.spiders',
 'REQUEST_FINGERPRINTER_IMPLEMENTATION': '2.7',
 'SPIDER_MODULES': ['bing.spiders'],
 'TWISTED_REACTOR': 'twisted.internet.asyncioreactor.AsyncioSelectorReactor'}
2024-02-10 15:29:59 [asyncio] DEBUG: Using selector: SelectSelector
2024-02-10 15:29:59 [scrapy.utils.log] DEBUG: Using reactor: twisted.internet.asyncioreactor.AsyncioSelectorReactor
2024-02-10 15:29:59 [scrapy.utils.log] DEBUG: Using asyncio event loop: asyncio.windows_events._WindowsSelectorEventLoop
2024-02-10 15:29:59 [scrapy.extensions.telnet] INFO: Telnet Password: 1be63b1fb2001178
2024-02-10 15:29:59 [scrapy.middleware] INFO: Enabled extensions:
['scrapy.extensions.corestats.CoreStats',
 'scrapy.extensions.telnet.TelnetConsole',
 'scrapy.extensions.logstats.LogStats']
2024-02-10 15:30:00 [scrapy.middleware] INFO: Enabled downloader middlewares:
['scrapy.downloadermiddlewares.httpauth.HttpAuthMiddleware',
 'scrapy.downloadermiddlewares.downloadtimeout.DownloadTimeoutMiddleware',
 'scrapy.downloadermiddlewares.defaultheaders.DefaultHeadersMiddleware',
 'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware',
 'scrapy.downloadermiddlewares.retry.RetryMiddleware',
 'scrapy.downloadermiddlewares.redirect.MetaRefreshMiddleware',
 'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware',
 'scrapy.downloadermiddlewares.redirect.RedirectMiddleware',
 'scrapy.downloadermiddlewares.cookies.CookiesMiddleware',
 'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware',
 'scrapy.downloadermiddlewares.stats.DownloaderStats']
2024-02-10 15:30:00 [scrapy.middleware] INFO: Enabled spider middlewares:
['scrapy.spidermiddlewares.httperror.HttpErrorMiddleware',
 'scrapy.spidermiddlewares.offsite.OffsiteMiddleware',
 'scrapy.spidermiddlewares.referer.RefererMiddleware',
 'scrapy.spidermiddlewares.urllength.UrlLengthMiddleware',
 'scrapy.spidermiddlewares.depth.DepthMiddleware']
2024-02-10 15:30:00 [scrapy.middleware] INFO: Enabled item pipelines:
[]
2024-02-10 15:30:00 [scrapy.core.engine] INFO: Spider opened
2024-02-10 15:30:00 [scrapy.extensions.logstats] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min)
2024-02-10 15:30:00 [scrapy.extensions.telnet] INFO: Telnet console listening on 127.0.0.1:6023
2024-02-10 15:30:00 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://cn.bing.com/hp/api/model> (referer: None)
2024-02-10 15:30:00 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://s.cn.bing.net/th?id=OHR.SpringFestival2024_ZH-CN7514007541_1920x1080.webp> (referer: https://cn.bing.com/hp/a
pi/model)
2024-02-10 15:30:00 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://s.cn.bing.net/th?id=OHR.StJamesPool_ZH-CN5930624359_1920x1080.webp> (referer: https://cn.bing.com/hp/api/mode
l)
2024-02-10 15:30:00 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://s.cn.bing.net/th?id=OHR.LakeBledSunrise_ZH-CN5580697031_1920x1080.webp> (referer: https://cn.bing.com/hp/api/
model)
2024-02-10 15:30:00 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://s.cn.bing.net/th?id=OHR.ChineseNewYearEve2024_ZH-CN7153418405_1920x1080.webp> (referer: https://cn.bing.com/h
p/api/model)
2024-02-10 15:30:00 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://s.cn.bing.net/th?id=OHR.DevetashkaCave_ZH-CN5186222166_1920x1080.webp> (referer: https://cn.bing.com/hp/api/m
odel)
2024-02-10 15:30:00 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://s.cn.bing.net/th?id=OHR.LakeTahoeRock_ZH-CN5770740919_1920x1080.webp> (referer: https://cn.bing.com/hp/api/mo
del)
2024-02-10 15:30:00 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://s.cn.bing.net/th?id=OHR.MtHoodOregon_ZH-CN6068357532_1920x1080.webp> (referer: https://cn.bing.com/hp/api/mod
el)
2024-02-10 15:30:00 [BingWallpaper] DEBUG: Saved file SpringFestival2024_ZH_CN7514007541_1920x1080 in bing_wallpapers
2024-02-10 15:30:00 [BingWallpaper] DEBUG: Saved file StJamesPool_ZH_CN5930624359_1920x1080 in bing_wallpapers
2024-02-10 15:30:00 [BingWallpaper] DEBUG: Saved file ChineseNewYearEve2024_ZH_CN7153418405_1920x1080 in bing_wallpapers
2024-02-10 15:30:00 [BingWallpaper] DEBUG: Saved file LakeBledSunrise_ZH_CN5580697031_1920x1080 in bing_wallpapers
2024-02-10 15:30:00 [BingWallpaper] DEBUG: Saved file DevetashkaCave_ZH_CN5186222166_1920x1080 in bing_wallpapers
2024-02-10 15:30:00 [BingWallpaper] DEBUG: Saved file LakeTahoeRock_ZH_CN5770740919_1920x1080 in bing_wallpapers
2024-02-10 15:30:00 [BingWallpaper] DEBUG: Saved file MtHoodOregon_ZH_CN6068357532_1920x1080 in bing_wallpapers
2024-02-10 15:30:00 [scrapy.core.engine] INFO: Closing spider (finished)
2024-02-10 15:30:00 [scrapy.statscollectors] INFO: Dumping Scrapy stats:
{'downloader/request_bytes': 2446,
 'downloader/request_count': 8,
 'downloader/request_method_count/GET': 8,
 'downloader/response_bytes': 1497452,
 'downloader/response_count': 8,
 'downloader/response_status_count/200': 8,
 'elapsed_time_seconds': 0.747333,
 'finish_reason': 'finished',
 'finish_time': datetime.datetime(2024, 2, 10, 7, 30, 0, 901089),
 'httpcompression/response_bytes': 27291,
 'httpcompression/response_count': 1,
 'log_count/DEBUG': 18,
 'log_count/INFO': 10,
 'request_depth_max': 1,
 'response_received_count': 8,
 'scheduler/dequeued': 8,
 'scheduler/dequeued/memory': 8,
 'scheduler/enqueued': 8,
 'scheduler/enqueued/memory': 8,
 'start_time': datetime.datetime(2024, 2, 10, 7, 30, 0, 153756)}
2024-02-10 15:30:00 [scrapy.core.engine] INFO: Spider closed (finished)

至此,一个简单的 Scrapy 爬虫就编写完了。

7.3 Scrapy 架构简介

7.3.1 Scrapy 架构图

Scrapy 的架构如图所示
image
架构中每个模块的作用如下。

  • Scrapy 引擎(Engine):Scrapy 框架的核心,用于控制数据流在框架内部的各个组件间流动,在相应动作发生时触发相关事件。
  • Scrapy 调度器(Scheduler):请求调度,从引擎接收到 Request,然后添加到队列中,在后续引擎请求它们时提供给引擎。
  • Scrapy 下载器(Downloader):获取到页面数据后提供给引擎,然后提供给爬虫。
  • 下载中间件(Downloader Middleware):下载器和引擎之间的特定钩子,可以在下载器把Response 传递给引擎前做一些处理。
  • 爬虫(Spider):产生 Request,交给下载器下载后返回 Response,解析提取出 Item或需要另外跟进的 URL 的类。
  • 爬虫中间件(Spider Middleware):爬虫和引擎之间的特定钩子,可以在 Spider 输入和输出两个阶段做一些处理。
  • 项目管道(Item Pipeline):用于处理爬虫产生的 Item,进行一些加工,如数据清洗、验证和持久化。

7.3.2 各个模块间的协作流程

了解了每个模块的作用后,我们来看看各个模块是如何进行协作的.
image

7.4 Spider 详解

利用 Scrapy Genspider 生成了一个爬虫项目,默认生成了一个爬虫类,然后可以执行 scrapy crawl 爬虫名命令来运行爬虫。其实还可以把 Spider 当作单独的Python 文件执行,直接使用 scrapy runspider 爬虫文件命令即可执行单个爬虫文件,示例如下:

scrapy runspider BingWallpaper.py

这也说明了 Spider 的重要性,其他的 Item 和 Pipline 可有可无,而 Settings 等也可以采用默认配置,唯独 Spider 需要我们自行编写,爬取站点的链接配置、抓取和解析逻辑都在Spider 中完成。

7.4.1 Spider 的主要属性和函数

前面已经介绍过 name、allowed_domains、start_urls 这三个属性和 parse()函数,下面介
绍其他内容。

  • start_requests()函数:对于固定的 URL,可以用 start_urls 存储,而有时我们需要对请求进行一些定制,比如使用 POST 请求、动态拼接参数、设置请求头等,就需要借助 start_requests()函数了。该函数必须返回一个可迭代对象(iterable),该对象包含用于爬取的第一个 Request。另外,要注意 Spider 中未指定start_urls 时,该函数才会生效。
  • custom_settings:字典,专属于本 Spider 类的配置,该配置会覆盖项目中的 settings.py的值,慎用。有些值覆盖了也不一定会起作用,该设置须在初始化前被更新,并且必须定义成类变量。
  • settings:settings 对象,利用它可以直接获取项目的全局设置变量。
  • crawler:定义 Spider 实例绑定的 crawler 对象,该属性在初始化 Spider 类时由from_crawler()函数设置,crawler 对象包含了很多项目组件,利用它可以获取项目的一些配置信息。
  • closed(reason):Spider 关闭时,该函数会被调用,参数是一个字符串,即结束原因,一般会在这个函数里写一些释放资源的收尾代码。

另外还有一点要注意,parse()函数作为默认的回调函数,在 Request 没有指定回调方法时会调用它,该回调方法的返回值只能是 Request、字典和 Item 对象,或者它们的可迭代对象。

7.4.2 Spider 运行流程

首先,了解 Scrapy 中第一个 Request 是如何产生的,先从源码层面来入手这个过程。
直接打开 Spider 类。start_urls 属性最先在__init__构造函数中出现:

def __init__(self, name=None, **kwargs):
    if name is not None:
        self.name = name
    elif not getattr(self, 'name', None):
        raise ValueError("%s must have a name" % type(self).__name__)
    self.__dict__.update(kwargs)
    if not hasattr(self, 'start_urls'):
        self.start_urls = []

hasattr()函数的作用是判断对象是否包含对应的属性,这里的代码就是检验是否设置了start_urls 属性,没有的话,初始化为一个空列表。接着,在 start_requests()函数中可以找到start_urls:

def start_requests(self):
    cls = self.__class__
    if method_is_overridden(cls, Spider, 'make_requests_from_url'):
        warnings.warn(
    "Spider.make_requests_from_url method is deprecated; it "
    "won't be called in future Scrapy releases. Please "
    "override Spider.start_requests method instead (see %s.%s)." % (cls.__module__, cls.__name__),
    )
    for url in self.start_urls:
        yield self.make_requests_from_url(url)
    else:
        for url in self.start_urls:
            yield Request(url, dont_filter=True)

上面的 warn()里的描述文字的大概意思是:make_requests_from_url 方法已经被弃用,
在以后的版本里不会再去调用此方法,请重写 start_requests 方法来代替。所以这里我们直接看 else 里的代码即可,遍历 start_urls 列表,然后为每个 URL 生成一个 Request 对象,交给 Scrapy 下载并返回 Response。另外,这个方法只会调用一次。再接着就是 parse()函数了:

def parse(self, response):
 raise NotImplementedError('{}.parse callback is not defined'.format (self.__class__.__name__))

parse()函数必须实现,否则会抛出NotImplementedError 异常,是 Request 类的默认回调函数,在该函数中处理 Response 类的默认回调函数,根据不同的返回结果,可以进行不同的后续操作。

  • 字典或 Item:通过 Feed Exports 等组件把返回结果写入到文件中,或者通过 Item Pipeline 进行数据清洗,然后写入数据库等。
  • 链接:有时解析后可能是一个 URL,在获取下一页或上一页时最常用,利用这个链接构造 Request 并设置新的回调函数,然后返回这个 Request,等待调度器调度。

通过不断重复解析 Response 获取数据,构造新的 Request,直到完成整个站点的爬取。

7.5 Request 类和 Response 类

在 Spider 中生成 Request 后,经过一系列的调度传递到 Downloader(下载器),下载器执行后返回一个 Response 对象,返回到发出请求的爬虫程序,然后在回调方法中解析这个Response,接下来我们详细介绍这两个类。

7.5.1 Request 详解

常用参数与方法如下。

  • url:设置请求的 URL。
  • method:设置请求方法,默认为 GET。
  • body:设置请求体。
  • callback:设置请求完成后返回的 Response 类的回调函数。
  • headers:设置请求的 Headers 数据。
  • cookies:设置页面的 Cookies,可以是 dict 或 list[dict]。
  • encoding:设置请求的转换编码。
  • priority:链接优先级,优先级越高,越优先爬取。
  • dont_filter:指定请求是否被 Scheduler 过滤(Scheduler 默认过滤重复请求),慎用。
  • errback:设置处理异常的回调函数。
  • copy():复制当前 Request。
  • replace():对 Request 对象参数进行替换。
  • meta:一个包含 Request 任意元数据的 dict,主要用于解析函数间的值传递、浅拷贝。

举个简单的例子:prase_1 函数中给 Item 某些字段提取了值,但另外一些值需要在parse_2 中提取,这时就需要把 parse_1 中的 Item 传递给 parse_2 进行处理,因为 parse_2 是回调函数,显然无法只以设置外参的方式传递,此时就可以利用 meta 这个属性了,最典型的就是爬取电商站点,大图都需要打开,能拿到商品名称和价格,但是还想获得单击商品后显示的大图,假设大图的链接在代码中,简单的示例代码如下:

def parse_1(self, response):
    item['title'] = response.css('xxx').extract_first()
    item['price'] = response.css('xxx').extract_first()
    next_url = response.css('xxx').extract_first()
    yield scrapy.Request(next_url, callback=self.parse_2, meta={'item': item})
def parse2(self, response):
        item = response.meta['item']
        item[imgae] = response.css('xxx').extract_first()
    return item

有一点要注意,Request.meta 属性可以包含任何数据,但是要注意一些特殊的键,避免重名Scrapy 及其内置扩展识别如下:

dont_redirect
dont_retry
handle_httpstatus_list
handle_httpstatus_all
dont_merge_cookies
cookiejar
dont_cache
redirect_urls
bindaddress
dont_obey_robotstxt
download_timeout
download_maxsize
download_latency
proxy

Request 的主要子类为 FormRequest,一般登录时会用到,实现对某些表单字段的预填充。通过 FormRequest.from_response()函数实现,示例代码如下:

# 从respongse返回一个request(FormRequest)
def parse(self, response):
 return scrapy.FormRequest.from_response( response,formdata={'user': 'jay', 'pawd': '12345'},callback=self.after_login)

7.5.2 Response 类常用参数、方法与子类

常用参数与方法如下。

  • url:响应对应的 URL。
  • status:响应对应的 HTTP 状态码,默认为 200。
  • body:请求返回的 HTML。
  • headers:获得请求头。
  • cookies:获得页面 Cookies。
  • request:获得生成此响应的 Request 对象。
  • Response 子类如下。
  • TextResponse:在 Response 的基础上添加编码能力,仅用于二进制数据,如图像、
  • 声音和媒体文件。
  • HtmlResponse:选择和提取 HTML 数据。
  • XmlResponse:选择和提取 XML 数据。

7.5.3 选择器

Scrapy 内置了一个 Selector 提取模块,支持Xpath 选择器、CSS 选择器和正则表达式,使用下述对应的方法解析即可。

# 下述两种方法返回一个SelectorList变量,是一个列表类型的数据,可以利用索引取出某个
# Selector元素,但是并不是真正的文本内容,可以调用extract()提取具体内容列表,或者调用
# extract_first()专门提取单一元素
response.xapth('xxx') # Xpath选择器
response.css('xxx') # CSS选择器
# 输出结果是正则表达式的分组,顺序输出,如果只想选取第一个元素,可以调用re_first()函数,
# 和普通正则表达式的find_all和find有点类似,另外要注意Response对象不能直接调用re()和re_first(),
# 可以先调用xpath()函数再进行正则匹配
response.xpath('xxx').re(xxx) # 正则表达式
# 另外Xpath选择器和CSS选择器支持嵌套选择,比如
response.Xpath("//tr").css("td")

7.5.4 Scrapy Shell

在编写 Spider 时,我们经常需要修改相关代码,以测试我们编写的 Xpath 和 CSS 表达式能否获取到正确的结果。每次都启动 Spider 显得有些烦琐,Scrapy 提供了一个交互终端,
可以在未启动 Spider 的情况下尝试调试爬取代码,命令如下:

scrapy shell url

以百度首页( https://www.baidu.com) 为例 ,部分结果如下:

scrapy shell "https://www.baidu.com"
2018-10-06 11:21:14 [scrapy.core.engine] INFO: Spider opened
2018-10-06 11:21:14 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://www.baidu.
com> (referer: None)
[s] Available Scrapy objects:
[s] scrapy scrapy module (contains scrapy.Request, scrapy.Selector, etc)
[s] crawler <scrapy.crawler.Crawler object at 0x10f52d8d0>
[s] item {}
[s] request <GET https://www.baidu.com>
[s] response <200 https://www.baidu.com>
[s] settings <scrapy.settings.Settings object at 0x110fb8b00>
[s] spider <DefaultSpider 'default' at 0x11125abe0>
[s] Useful shortcuts:
[s] fetch(url[, redirect=True]) Fetch URL and update local objects (by default, redirects are 
followed)
[s] fetch(req) Fetch a scrapy.Request and update local objects
[s] shelp() Shell help (print this help)
[s] view(response) View response in a browser

Scrapy Shell 根据下载页面自动创建了一些对象供我们使用。

  • scrapy:Scrapy 模块。
  • crawler:当前的 crawler 对象。
  • request:最近获取到的页面的 Request 对象。
  • response:最近获取到的页面的 Response 对象。
  • settings:当前的 Scrapy 项目的 settings.py。
  • spider:处理 URL 的 Spider。

还有一些快捷命令如下。

  • shelp():打印可用对象及快捷命令的帮助列表。
  • fetch(url 或 request):根据给定的 URL 或 Request 获取到一个 Response,并更新相关对象。
  • view(response):在浏览器中打开给定的 Response,会在 Response 的 body 中添加一个 tag,使外部链接(图片、CSS 等)能正常显示。

7.6 Item 详解

Scrapy 提供了一个公共的数据输出格式类 Item,和 Python 中的字典类似,但是多了一些额外的保护机制,避免拼写错误和定义字段错误,字段使用 scrapy.Field 赋值。使用代码示例如下:

class MyItem(scrapy.item):
    name = scrapy.Field()
    age = scrapy.Field()
    # 创建
    my_item = MyItem(name = "小猪", age = 25)
    # 获取字段
    my_item['name'] # 如果不存在此字段,会报KeyError错误
    my_item.get('name') # 如果不存在此字段,返回none
    'name' in my_item
    'name' in my_item.fields
    # 设置字段值
    my_item['name'] = "小杰"
    # 访问所有的填充值,和字典一样的API
    product.keys = ['name', 'age']
    product.items = [('name','小杰'),('age',25)]
# Item继承(加入新字段或改动某些字段的元数据)
class MySuperItem(MyItem):
    sex = scrapy.Field()
    age = scrapy.Field(serializer = str)

7.7 Item Pipeline 详解

Item Pipeline 的调用发生在 Spider 解析完 Response 产生 Item 后,会把 Item 传递给项目管道,可以在管道中完成一连串的处理,比如数据清洗、去样重复数据,最后将处理的结果持久化到本地(写入文件或保存到数据库中)。

7.7.1 自定义 Item Pipeline 类

在 pipelines.py 文件中定义一个 Pipeline 类,有如下几个核心方法。

  • process_item(self, item, spider):必须实现,处理 Item 的方法,在这里进行数据处理和持久化操作。
  • open_spider(spider):Spider 开启时自动调用,可以在此做一些初始化操作,比如创建数据库连接等。
  • close_spider(spider):Spider 关闭时自动调用,可以在此做一些收尾操作,比如关闭数据库连接等。
  • from_crawler(cls, crawler):类方法,用@classmethod 标识,一种依赖注入方式,参数是 crawler。通过它,我们可以拿到 Scrapy 的所有核心组件,比如全局信息。

代码示例如下:

class BcyPipeline():
    def __init__(self):
        self.host = 'localhost'
        self.database = 'bcy'
        self.user = 'root'
        self.password = 'Zpj12345'
        self.port = 3306
    def open_spider(self, spider):
        self.db = pymysql.connect(self.host, self.user, self.password, self.database, 
        charset='utf8', port=self.port)
        self.cursor = self.db.cursor()
    def close_spider(self, spider):
        self.db.close()
    def process_item(self, item, spider):
        data = dict(item)
        keys = ', '.join(data.keys())
        values = ', '.join(["%s"] * len(data))
        sql = "INSERT INTO draw (%s) VALUES (%s)" % (keys, values)
        self.cursor.execute(sql, tuple(data.values()))
        self.db.commit()
        return item

7.7.2 启用 Item Pipeline

创建完自定义的 Item Pipeline 后,我们还需要在 settings.py 文件中把 ITEM_PIPELINES注释打开,并加上我们的 Item Pipeline,代码梳理如下:

ITEM_PIPELINES = {
'Bcy.pipelines.BcyPipeline':300
}

这里 300 代表执行的先后顺序,Item 按数字从小到大的顺序通过 Item Pipeline,通常在 1~1000 之间。

7.8 实战:完善爬取每日壁纸的脚本

实战案例中我们已经用 Scrapy 爬取到图片链接了,接下来完善代码,利用 Scrapy 自带的ImagesPipeline 完成图片的下载。

7.8.1 定义 BingItem

要使用 ImagesPipeline 下载图片,我们需要先定义一个 BingItem,items.py 文件定义了 BingItem,完整代码如下:

import scrapy

class BingItem(scrapy.Item):
    image_url = scrapy.Field()
    filename = scrapy.Field()

7.8.2 使用 ImagesPipeline

ImagesPipeline 的工作流程如下:
(1)把图片 URL 放到 image_urls 中,image_urls 传入的必须是一个可迭代对象,而不
能是字符串。
(2)爬虫回调处理完返回 Item,进入项目管道。
(3)项目进入 ImagePipeline,image_urls 组内的 URL 将被 Scrapy 的调度器和下载器安排下载。
(4)图片下载结束后,图片下载路径、URL 等信息会保存到 images 字段中。
在 pipelines.py 文件中,你可以编写一个下载图片的管道;

import scrapy
from scrapy.pipelines.images import ImagesPipeline
from scrapy.exceptions import DropItem
from scrapy.http import Request
from urllib.parse import urlparse

class BingImagesPipeline(ImagesPipeline):
    def get_media_requests(self, item, info):
        yield Request(item['image_url'])

    def item_completed(self, results, item, info):
        image_paths = [x['path'] for ok, x in results if ok]
        if not image_paths:
            raise DropItem("Item contains no images")
        return item

打开 settings.py 启用 ImagesPipeline 组件:

ITEM_PIPELINES = {
    'bing.pipelines.BingImagesPipeline': 1,
}

# 设置图片存储路径
IMAGES_STORE = 'downloaded_images/'

7.8.3 修改 Spider 代码

修改爬虫代码提取 URL,新建一个 BingItem()实例,image_urls 传入 URL 列表,最后yield 返回这个 Item 实例,具体代码如下:

import scrapy
import json
import re
from bing.items import BingItem  # 导入自定义的BingItem

class BingWallpaperSpider(scrapy.Spider):
    name = "BingWallpaper"
    allowed_domains = ["cn.bing.com", "cn.bing.net", "s.cn.bing.net"]
    start_urls = ["https://cn.bing.com/hp/api/model"]
    ROBOTSTXT_OBEY = False

    def parse(self, response):
        # 将响应内容解析为 JSON 格式
        data_json = json.loads(response.body)
        for img in data_json['MediaContents']:
            img_url = img['ImageContent']['Image']['Url']
            img_url = img_url.split('th')[1]
            filename = img_url.split('.')[1]
            content = 'https://s.cn.bing.net/th' + img_url
            # 清除文件名中的非法字符
            filename = re.sub('[^0-9a-zA-Z]+', '_', filename)

            # 创建BingItem对象并传递数据
            item = BingItem()
            item['image_url'] = content
            item['filename'] = filename
            yield item

7.8.4 运行爬虫脚本

编写完成后准备执行爬虫,通过下述命令行来启动爬虫的:

scrapy crawl BingWallpaper

每次运行调试都需要输入一遍命令,有些烦琐,我们可以在项目中编写一个 py 文件来执行这个命令,然后每次运行这个 py 文件就可以了,比如在项目中新建一个 run.py 文件(该文件必须和scrapy.cfg 文件在同一目录下)

from scrapy import cmdline
cmdline.execute(["scrapy", "crawl", "BingWallpaper"])

以后直接运行这个 run.py 就可以了,而不需要再打开命令行手动输入.
打开对应的文件夹,壁纸都下载到本地了.

7.9 设置请求头

Scrapy 默认的请求头格式是:Scrapy/1.1.2(+http://scrapy.org),看到这样的请求头,服务器可能直接把这个爬虫封掉,所以我们需要修改请求头,有下述几种修改请求头的方法。

7.9.1 构造 Request 时传入

我们可以在构造 Request 时通过 headers 参数传入,示例如下:

headers = {
 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
 'Chrome/68.0.3440.106 Safari/537.36',
 'Host': 'bcy.net',
 'Origin': 'https://bcy.net',
}
def start_requests(self):
 yield Request(url, headers= headers, callback=parse)

7.9.2 修改 settings.py 文件

除了上面设置请求头的方式外,还可以在settings.py 文件中进行全局设置,对所有爬虫都会生效,使用示例如下:

DEFAULT_REQUEST_HEADERS = {
 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
 'Chrome/68.0.3440.106 Safari/537.36',
 'Host': 'bcy.net',
 'Origin': 'https://bcy.net',
}

7.9.3 为爬虫添加 custom_settings 字段

还有一种方法是在爬虫中添加 custom_settings 字段,示例如下:

custom_settings = {
 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
 'Chrome/68.0.3440.106 Safari/537.36',
}

7.10 下载中间件详解

Downloader Middleware(下载中间件)是处于 Request 和 Response 之间的处理模块。该模块作用于 Request 执行下载前和生成 Response 被 Spider 解析前。在 Scrapy 中有两个过程会经过下载中间件:一是 Scheduler 从队列里取出Request 给 Downloader 执行下载时;二是Downloader 下载时得到 Response 返回给Spider 时。

7.10.1 自定义 Downloader Middleware 类

主要实现以下三个核心方法。

  • process_request(request, spider):Downloader 下载执行前调用。
  • process_response(request, response, spider):生成 Response 在被 Spider 解析前调用。
  • process_exception(request, response, spider):Downloader 或 process_request()函数抛出异常时调用。
    使用 Scrapy 的第一个感觉就是快,很快就访问了很多请求,但是问题来了,这样快速、频繁地请求,可能会导致我们的 IP 被服务器 block,我们需要为请求设置代理,这里自定义一个代理下载中间件。另外,有一点要注意:并不是每个请求都直接通过代理,因为使用代理访问站点的速度比正常访问慢多了。
    所以,在第一次请求失败后才启用代理,这一步可以通过判断 retry_times 是否为空。打开 middlewares.py 文件,新增一个类ProxyMiddleware,代码如下:
class ProxyMiddleware(object):
    def __init__(self):
        self.proxy_ip_list = self.load_list_from_file()

    @staticmethod
    def load_list_from_file():
        data_list = []
        with open(os.path.join(os.getcwd(), 'FirstSpider/proxy_ip.txt'), "r+", encoding='utf-8') as f:
            for ip in f:
                data_list.append(ip.replace("\n", ""))
        return data_list

    def process_request(self, request, spider):
        if request.meta.get('retry_times'):
            proxy = random.choice(self.proxy_ip_list)
            if proxy:
                proxy_ip = 'https://{proxy}'.format(proxy=proxy)
                logging.debug("使用了代理:", proxy_ip)
                request.meta['proxy'] = proxy_ip

7.10.2 启用自定义的代理下载中间件

直接打开 settings.py 文件,启用中间件:

DOWNLOADER_MIDDLEWARES = {
    'FirstSpider.middlewares.ProxyMiddleware': 543,
}

7.11 实战:爬取某电商网站

7.11.1 分析爬取的站点

爬取的目标是某电商网站图书畅销榜:
http://bang.dangdang.com/books/bestsellers/01.00.00.00.00.00-recent7-0-0-1-1
作者名称和其他详细信息,并保存到 MySQL 数据库中。

7.11.2 新建项目与明确爬取目标

通过命令行创建 Scrapy 项目:

scrapy startproject dangdang

7.11.3 创建爬虫爬取网页

通过命令行创建爬虫脚本:

scrapy genspider dang "www.dangdang.com"

修改爬虫

import scrapy
import parsel

class DangSpider(scrapy.Spider):
    name = "Dang"
    allowed_domains = ["www.dangdang.com"]
    start_urls = ["http://bang.dangdang.com/books/bestsellers/01.00.00.00.00.00-recent7-0-0-1-{}"]
    page_count = 5  # 设置需要爬取的页数

    def start_requests(self):
        for page in range(1, self.page_count + 1):
            url = self.start_urls[0].format(page)
            yield scrapy.Request(url, callback=self.parse)

    def parse(self, response):
        charset = response.encoding
        data = response.body.decode(charset, errors='replace')
        selector = parsel.Selector(data)
        lis = selector.css('ul.bang_list li')
        for li in lis:
            item = {}
            item['title'] = li.css('.name a::attr(title)').get()
            item['comment'] = int(li.css('.star a::text').get().replace('条评论', ''))
            recommend_str = li.css('.star .tuijian::text').get().replace('推荐', '')
            recommend = float(recommend_str.strip('%')) / 100
            item['recommend'] = recommend
            item['author'] = li.css('.publisher_info a:nth-child(1)::attr(title)').get()
            item['publish'] = li.css('div:nth-child(6) a::text').get()
            item['price_n'] = li.css('.price .price_n::text').get()
            price_n_str = item['price_n'].replace('¥', '')  # 去掉货币符号
            price_n = float(price_n_str)  # 转换为浮点数
            item['price_n'] = price_n
            item['href'] = li.css('.name a::attr(href)').get()

            yield item

在items.py 写入迭代对象

import scrapy

class DangdangSpiderItem(scrapy.Item):
    title = scrapy.Field()
    comment = scrapy.Field()
    recommend = scrapy.Field()
    author = scrapy.Field()
    publish = scrapy.Field()
    price_n = scrapy.Field()
    href = scrapy.Field()

7.11.4 存储数据

在拿到数据后,接着把这些数据存储到 MySQL 数据库中,我们需要自定义一个 Item ,,Pipeline 类,在 pipelines.py 类中新增一个 MySQLPipeline 管道类,用于持久化数据。

import pymysql

class MySQLPipeline(object):
    def open_spider(self, spider):
        # 连接到MySQL数据库
        self.conn = pymysql.connect(
            host='localhost',
            port=3306,
            database='test',
            user='root',
            password='root',
            charset='utf8mb4'
        )
        # 创建游标对象
        self.cursor = self.conn.cursor()

    def close_spider(self, spider):
        # 关闭游标和数据库连接
        self.cursor.close()
        self.conn.close()

    def process_item(self, item, spider):
        insert_sql = """
        INSERT INTO books (title, comment, recommend, author, publish, price_n, href)
        VALUES (%s, %s, %s, %s, %s, %s, %s)
        """
        data = (
            item['title'],
            item['comment'],
            item['recommend'],
            item['author'],
            item['publish'],
            item['price_n'],
            item['href']
        )
        self.cursor.execute(insert_sql, data)
        self.conn.commit()

        return item

接着修改 settings.py 文件启用MySQLPipeline,启用代码如下:

ROBOTSTXT_OBEY = False
ITEM_PIPELINES = {
    'dangdang.pipelines.MySQLPipeline': 300,
}

建立保存数据的数据库

create  database  test;
CREATE TABLE books (
    id INT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(255),
    comment INT,
    recommend FLOAT,
    author VARCHAR(255),
    publish VARCHAR(255),
    price_n FLOAT,
    href VARCHAR(255)
);

查询数据库结果

select * from books

image

使用此类脚本下载网站内容时应遵守网站的使用条款,以及相关的法律法规。
本系列文章皆做为学习使用,勿商用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值