p.s.高产量博主,点个关注不迷路!(文章较长,赶时间可以点个收藏或直接跳转完整源码)
目录
I. 实战需求分析与思路
首先,笔记承接上一篇,我们知道一个完整的scrapy框架项目文件有六个部分:
1️⃣ Spiders文件夹:这文件夹我们不陌生,因为每一次新建scrapy爬虫项目后,我们都需要终端进入Spiders文件夹,生产爬虫文件。在Spiders文件夹下,又有两个文件,一个是_init_.py文件,一个是tc.py。_init_.py文件是我们创建项目时默认生成的一个py文件,我们用不到这个py文件,因此我们可以忽略它,另一个tc.py文件是我们爬虫的核心文件,后续的大部分代码都会写入这个文件,因此它是至关重要的py文件。
2️⃣_init_.py文件:它和上面提到的Spiders文件夹下的_init_.py一样,都是不被使用的py文件,无需理会。
3️⃣ items.py文件:这文件定义了数据结构,这里的数据结构与算法中的数据结构不同,它指的是爬虫目标数据的数据组成结构,例如我们需要获取目标网页的图片和图片的名称,那么此时我们的数据组成结构就定义为 图片、图片名称。后续会专门安排对scrapy框架定义数据结构的学习。
4️⃣ middleware.py文件:这py文件包含了scrapy项目的一些中间构件,例如代理、请求方式、执行等等,它对于项目来说是重要的,但对于我们爬虫基础学习来说,可以暂时不考虑更改它的内容。
5️⃣ pipelines.py文件:这是我们之前在工作原理中提到的scrapy框架中的管道文件,管道的作用是执行一些文件的下载,例如图片等,后续会安排对scrapy框架管道的学习,那时会专门研究这个py文件。
6️⃣ settings.py文件:这文件是整个scrapy项目的配置文件,里面是很多参数的设置,我们会偶尔设计到修改该文件中的部分参数,例如下一部分提到的ROBOTS协议限制,就需要进入该文件解除该限制,否则将无法实现爬取。
本次笔记重点针对于pipelines.py文件与items.py文件的使用进行讲解,并辅以实战。首先我们简单做一下实战的需求分析:
我们打开当当网的首页,之后任意点击一种图书,例如这里点击【青春文学】:
之后可以再选择一个子类,例如我们选择【爱情/情感】:
之后我们的实战需求是把最终打开的这个页面中的图书图片、价格、图片名称一起下载下来,要求必须同步下载数据(开启多管道),并且能实现多页下载。
于是我们可以做简单的分析:
首先,我们需要创建项目文件,生成爬虫文件;接下来,从response中提取我们需要的数据;最后是把数据放入管道下载,这是一个简单的思路,省略了一些步骤,但总体上来看思路是可行的。
最后我们应该能获得:
II. 接口的获取与scrapy项目的创建
接下来,我们开始创建项目:
打开之前创建过的文件夹,用终端进入这个文件夹(如果之前笔记没有看过的朋友,直接新建一个空文件夹即可,之后用终端进入该文件夹):
运行项目生成指令:
scrapy startproject dangdang
之后我们不着急新建爬虫文件,因为此时我们缺少目的地的url,于是我们需要简单的抓一下接口:
这个接口不需要通过F12检查网页,只需要直接复制刚才进入的【爱情/情感】页面的url即可(通过F12也可以找到接口,接口就是这个页面的url,因此无需用F12):
http://category.dangdang.com/cp01.01.02.00.00.00.html
拿到之后,我们可以生成爬虫文件:
首先终端进入Spiders文件夹:
cd scrapy_dangdang/scrapy_dangdang/spiders
运行爬虫文件生成指令:
scrapy genspider dangdangwang http://category.dangdang.com/cp01.01.02.00.00.00.html
其中 "dangdangwang" 是生成的爬虫文件的文件名,大家可以任意起名。
正确生成后,我们可以用pycharm打开项目文件,打开后我们点击爬虫文件dangdangwang.py,先把默认多生成的http://头和html后面的斜线都删去,否则影响它的工作。
最后我们的基础操作还剩下一步:上次笔记提到了我们有一个参数:allowed_domains,这是我们整个项目爬取的url的范围,由于本次我们的需求是爬去三种数据,无法固定某一个url,因此我们修改allowed_domains的参数为:
allowed_domains = ['category.dangdang.com']
此时它代表了整个的域名,也就是域名下的其他子域名或者子url都被包括,这个操作在大型项目中也经常会遇到,我们只需要把allowed_domains的参数修改成目的网站的域名即可!
III.items数据结构文件配置
接下来,我们需要配置一下items数据结构文件,此时我们先不要尝试理解它,而是先去做:
打开items.py文件:
我们刚才提到了一共要下载三样东西:图片、图片的名称以及图片对应图书的单价,于是我们可以理解成我们需要下载的目标数据结构有三种类型:图片(src)、图片名称和单价。
那么我们可以在items.py文件中写入:
# 图片的url
src = scrapy.Field()
# 图片名称
name = scrapy.Field()
# 价格
price = scrapy.Field()
它的格式是:数据类型的名称 = scrapy.Field(),理解起来可能初学不太容易,但是我们可以先这么写,这些内容统统写在 class ScrapyDangdangItem(scrapy.Item): 下面:
IV. 爬虫文件的书写
定义数据结构之后,我们可以开始准备写核心爬虫文件,在此之前,我们还需要一项工作:
分析页面源码,并得出xpath解析语句,解析之前提到的三种数据:
我们回到刚才的当当网页面,按F12解析页面,选择元素检查,把鼠标放到第一本书的图片上:
可以看出,我们需要的图片src和名称分别在img标签的src和alt属性中,于是xpath语法是这样的:
//ul[@id = "component_59"]/li//img/@src
//ul[@id = "component_59"]/li//img/@alt
但是在当当网中,有个小陷阱,那就是当我们把鼠标放在第二张图时,可以发现它的图片的src并不在src属性下,而是在data-original中:
于是针对第一张图片的src,我们可以采用上面的xpath语法,其他的页面的图片src,我们用下面这句语法:
//ul[@id = "component_59"]/li//img/@data-original
图书的价格,我们发现可以用这句语法拿到:
//ul[@id = "component_59"]/li//p[@class = "price"]/span[1]/text()
xpath语句准备好之后,我们开始编写爬虫文件:
打开dangdangwang.py文件,代码书写的区域应该聚焦在parse(self,response)这个函数中(原因不做赘述,可以参考上一篇笔记):
我们先简化一下目标,把多页爬取先简化成爬取第一页的上述三种数据,那么关于响应与提取数据部分的代码应该是这样的:
def parse(self, response):
# 所有的selector对象都可以调用xpath方法
li_list = response.xpath('//ul[@id = "component_59"]/li')
# 判断一下,第一张图是src,第二张图开始在data-original里
for li in li_list:
src = li.xpath('.//img/@data-original').extract_first()
if src:
src = src
else:
src = li.xpath('.//img/@src').extract_first()
name = li.xpath('.//img/@alt').extract_first()
price = li.xpath('.//p[@class = "price"]/span[1]/text()').extract_first()
这部分需要做一个简单的注解:
按照传统的思路,例如我们要在scrapy框架下,用xpath解析目的地的图片,那么我们应该直接写上:
src = response.xpath('//ul[@id = "component_59"]/li//img/@src或@data-original')
但是这里我们没有采用这种直接的方式,而是首先用一个xpath语句:
li_list = response.xpath('//ul[@id = "component_59"]/li')
然后对这个li_list进行二次xpath解析。这是在scrapy框架下的一种特有的写法,它的依据是在scrapy框架下,.xpath()方法并不会直接返回我们的数据列表,而是会返回一个selector对象(上一篇笔记中有解释),而对于一个selector对象,可以再次xpath,于是有了这种写法:先总体xpath,解析后对每一个部分,继续在剩余的部分中进行xpath解析。
V. 管道的配置
完成xpath解析后,拿到了需要的数据。根据我们的整体思路,这些数据中的一部分需要放到管道中执行下载。回想scrapy框架的工作原理,管道负责下载一些文件,这里我们实战中,这个文件指的是图书的图片、图书的价格和名称(保存在json文件)!接下来,我们开始学习管道的配置:
1️⃣ 首先,我们需要封装一下管道文件:
打开pipelines.py,并把注意力放在这个class中,更确切地说是process_item()函数中:
这函数是干什么的呢?是在爬虫文件调用了管道(调用的代码后面会再解释,这里先知道是当调用的时候)时,会执行的一个函数,我们可以简单理解为被爬虫文件调用的某个函数。这个函数的主要功能就是下载。
那我们首先先解决把图片的src、图片名称和图片价格三个数据写入json文件中并下载json文件这项任务,下载图片后续再处理。
class ScrapyDangdangPipeline:
def open_spider(self, sipder):
self.fp = open('book.json','w',encoding = 'utf-8')
def process_item(self, item, spider):
self.fp.write(str(item))
return item
def close_spider(self,spider):
self.fp.close()
这可以由上面的代码实现。解释一下上面的部分:
首先,我们原来只有一个process_item()函数,但是如果只有一个下载的函数,我们每次向文件写入后都要执行文件关闭,下一次执行文件的打开,这会导致频繁的操作文件的打开与关闭,不利于我们的目的,于是我们采用另一种方法:在管道文件中,除了process_item()函数外,还有两个函数,分别是:open_spider()和close_spider(),这两个函数不会被初始化生成,但是我们可以手动添加这两个函数,它们的特点是:
open_spider()函数和close_spider()函数分别在项目的启动和终止时各自被调用一次。
于是我们通过这个特点,在open_spider()函数中打开文件,在close_spider()中关闭文件,在process_item()中执行写入操作,即可避免频繁的文件打开关闭。(使用self.fp能保证三个函数操作的是同一个文件对象)
比葫芦画瓢,由于要下载的还有图片,我们在初始的class下面自行定义一个新的class,并在class下定义process_item()函数,在这个process_item()函数中写上图片的下载操作:
import urllib.request
# 图片下载管道:
class DangDangDownloadImgPipeline:
def process_item(self, item, spider):
url = 'http:' + item.get('src') # 这里是因为item本身是获取的json数据,存在字典里,所以我们用字典的get函数获取
filename = './books/' + item.get('name') + '.jpg'
urllib.request.urlretrieve(url = url,filename = filename)
return item
别忘了导入urllib.request库,我们是通过这个库下载图片的(这个如果不知道的话,可以去看之前的笔记)。
所以现在的pipelines.py文件应该是这样的:
2️⃣ 修改settings.py文件,使管道可以被使用:
在第一步中,我们为下载图片,新定义了一个class,那么可能会有一个疑惑点:我们新建的class会被项目运行吗?这里给出明确的答案:如果没有第二步修改settings.py文件,那么新建的class下的代码"不会"被执行,而且甚至初始的class也不会被执行。所以第二步是必不可少的,也是容易被忽略的。
我们打开settings.py文件,找到有一个被注释了的ITEM_PIPELINES字典,这就是定义管道的地方,我们首先需要解除它的注释:
这个字典中,键值对的值是一个300,它的意思是管道的优先级,也就是下载的优先级 ,这个值在1-1000中,值越小,优先级越高!
我们复制这个字典对象,然后在它下方粘贴一下,把pipeline.ScrapyDangdangPipeline改成我们自己起的类名:
到这里,我们完成了管道文件的配置,不过还没有完成全部内容,我们还需要填一个坑:在爬虫文件中调用管道文件。
3️⃣ 在爬虫文件中调用管道文件:
首先,我们需要在爬虫文件中导入之前定义的items数据结构,导入的格式是这样的:
from 项目名.items import items.py文件中的类名
对于本项目,应该是这样的:
from scrapy_dangdang.items import ScrapyDangdangItem
导入后 是这样的:
这之后,可能会爆红线错误,但是我们不需要理会,继续做就好。
接下来,在爬虫文件中紧挨着刚才写的xpath解析的下一行,写上两句代码:
book = ScrapyDangdangItem(src = src,name = name,price = price)
# 获取一个book对象,就传入pipeline进行下载
yield book
第一句代码,是生成一个数据结构对应的对象,我们需要调用刚才导入的items文件下的ScrapyDangdangItem()类,这部分对于学过面向对象编程的朋友来说不难理解。
第二句代码是真正调用了管道,这句代码执行后,管道就被调用去下载文件。
因此此时我们的爬虫文件是这样的:
VI. 多页下载处理
最后,接近本次实战的尾声,我们对多页下载需求进行一个满足:
多页时,我们首先需要在爬虫文件中修改allowed_domains为当当网的域名,这一点之前已经提到,也已经修改了,于是不做赘述。
接下来,我们发现当当网除了第一页之外,其他页面的url有下面的规律:
http://category.dangdang.com/pg页码-cp01.01.02.00.00.00.html
也就是说我们下载了第一页之后,后面的页面的url可以由http://category.dangdang.com/pg + 页码 + -cp01.01.02.00.00.00.html生成。
于是我们首先在爬虫文件最上面定义全局变量:初始url段和页码page
base_url = 'http://category.dangdang.com/pg'
page = 1
在parse()函数的for循环外,写上下面的代码:
if self.page < n:
self.page = self.page + 1
url = self.base_url + str(self.page) + '-cp01.01.02.00.00.00.html'
# 调用parse函数
# scrapy.Request是scrapy的get请求:
# url是请求地址,callback是要执行的函数,不需要加圆括号
yield scrapy.Request(url = url,callback = self.parse)
解释一下这段代码:
我们在for循环外判断当前的page是否小于某个数n,n代表我们想要下载多少页,之后我们拼接出url,用yield关键字,我们可以调用一个叫Request的函数,这个函数是在执行了第一次Request请求后会调用的函数,可理解成是一个回调函数,在这个回调函数中,传参是url和需要回调的具体函数,我们很自然填入拼接后的url,回调函数我们就继续回调本函数parse即可。
也即这样的逻辑:项目第一次执行Request请求 - - - > 请求成功,回调了scrapy.Request()参数中的callback函数 - - - > callback函数仍然会发起第二次Request请求,直至page等于n结束。
VII. 完整源码
最后附上本次实战的源码:
1️⃣ 爬虫文件dangdang.py:
import scrapy
from scrapy_dangdang.items import ScrapyDangdangItem
class DangdangwangSpider(scrapy.Spider):
name = 'dangdangwang'
allowed_domains = ['category.dangdang.com']
start_urls = ['http://http://category.dangdang.com/cp01.01.02.00.00.00.html/']
base_url = 'http://category.dangdang.com/pg'
page = 1
def parse(self, response):
li_list = response.xpath('//ul[@id = "component_59"]/li')
# 判断一下,第一张图是src,第二张图开始在data-original里
for li in li_list:
src = li.xpath('.//img/@data-original').extract_first()
if src:
src = src
else:
src = li.xpath('.//img/@src').extract_first()
name = li.xpath('.//img/@alt').extract_first()
price = li.xpath('.//p[@class = "price"]/span[1]/text()').extract_first()
book = ScrapyDangdangItem(src = src,name = name,price = price)
# 获取一个book对象,就传入pipeline进行下载
yield book
# 多页爬取时,单页的逻辑是相通的,只需要把页的页码请求再次调用parse()函数即可
if self.page < 5:
self.page = self.page + 1
url = self.base_url + str(self.page) + '-cp01.01.02.00.00.00.html'
yield scrapy.Request(url = url,callback = self.parse)
2️⃣ settings.py文件:
# Scrapy settings for scrapy_dangdang project
#
# For simplicity, this file contains only settings considered important or
# commonly used. You can find more settings consulting the documentation:
#
# https://docs.scrapy.org/en/latest/topics/settings.html
# https://docs.scrapy.org/en/latest/topics/downloader-middleware.html
# https://docs.scrapy.org/en/latest/topics/spider-middleware.html
BOT_NAME = 'scrapy_dangdang'
SPIDER_MODULES = ['scrapy_dangdang.spiders']
NEWSPIDER_MODULE = 'scrapy_dangdang.spiders'
# Crawl responsibly by identifying yourself (and your website) on the user-agent
#USER_AGENT = 'scrapy_dangdang (+http://www.yourdomain.com)'
# Obey robots.txt rules
ROBOTSTXT_OBEY = True
# Configure maximum concurrent requests performed by Scrapy (default: 16)
#CONCURRENT_REQUESTS = 32
# Configure a delay for requests for the same website (default: 0)
# See https://docs.scrapy.org/en/latest/topics/settings.html#download-delay
# See also autothrottle settings and docs
#DOWNLOAD_DELAY = 3
# The download delay setting will honor only one of:
#CONCURRENT_REQUESTS_PER_DOMAIN = 16
#CONCURRENT_REQUESTS_PER_IP = 16
# Disable cookies (enabled by default)
#COOKIES_ENABLED = False
# Disable Telnet Console (enabled by default)
#TELNETCONSOLE_ENABLED = False
# 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',
#}
# Enable or disable spider middlewares
# See https://docs.scrapy.org/en/latest/topics/spider-middleware.html
#SPIDER_MIDDLEWARES = {
# 'scrapy_dangdang.middlewares.ScrapyDangdangSpiderMiddleware': 543,
#}
# Enable or disable downloader middlewares
# See https://docs.scrapy.org/en/latest/topics/downloader-middleware.html
#DOWNLOADER_MIDDLEWARES = {
# 'scrapy_dangdang.middlewares.ScrapyDangdangDownloaderMiddleware': 543,
#}
# Enable or disable extensions
# See https://docs.scrapy.org/en/latest/topics/extensions.html
#EXTENSIONS = {
# 'scrapy.extensions.telnet.TelnetConsole': None,
#}
# Configure item pipelines
# See https://docs.scrapy.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES = {
'scrapy_dangdang.pipelines.ScrapyDangdangPipeline': 300,
}
ITEM_PIPELINES = {
'scrapy_dangdang.pipelines.DangDangDownloadImgPipeline': 300,
}
# Enable and configure the AutoThrottle extension (disabled by default)
# See https://docs.scrapy.org/en/latest/topics/autothrottle.html
#AUTOTHROTTLE_ENABLED = True
# The initial download delay
#AUTOTHROTTLE_START_DELAY = 5
# The maximum download delay to be set in case of high latencies
#AUTOTHROTTLE_MAX_DELAY = 60
# The average number of requests Scrapy should be sending in parallel to
# each remote server
#AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0
# Enable showing throttling stats for every response received:
#AUTOTHROTTLE_DEBUG = False
# Enable and configure HTTP caching (disabled by default)
# See https://docs.scrapy.org/en/latest/topics/downloader-middleware.html#httpcache-middleware-settings
#HTTPCACHE_ENABLED = True
#HTTPCACHE_EXPIRATION_SECS = 0
#HTTPCACHE_DIR = 'httpcache'
#HTTPCACHE_IGNORE_HTTP_CODES = []
#HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage'
3️⃣ pipelines.py文件:
# 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
# useful for handling different item types with a single interface
from itemadapter import ItemAdapter
class ScrapyDangdangPipeline:
def open_spider(self, sipder):
self.fp = open('book.json','w',encoding = 'utf-8')
def process_item(self, item, spider):
self.fp.write(str(item))
return item
def close_spider(self,spider):
self.fp.close()
import urllib.request
# 图片下载管道:
class DangDangDownloadImgPipeline:
def process_item(self, item, spider):
url = 'http:' + item.get('src') # 这里是因为item本身是获取的json数据,存在字典里,所以我们用字典的get函数获取
filename = './books/' + item.get('name') + '.jpg'
urllib.request.urlretrieve(url = url,filename = filename)
return item
到此,本次实战完结!