第 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。通过搜索图片的名称。
将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 的架构如图所示
架构中每个模块的作用如下。
- 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 各个模块间的协作流程
了解了每个模块的作用后,我们来看看各个模块是如何进行协作的.
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
使用此类脚本下载网站内容时应遵守网站的使用条款,以及相关的法律法规。
本系列文章皆做为学习使用,勿商用。