一.Scrapy框架简介
何为框架,就相当于一个封装了很多功能的结构体,它帮我们把主要的结构给搭建好了,我们只需往骨架里添加内容就行。scrapy框架是一个为了爬取网站数据,提取数据的框架,我们熟知爬虫总共有四大部分,请求、响应、解析、存储,scrapy框架都已经搭建好了。scrapy是基于twisted框架开发而来,twisted是一个流行的事件驱动的python网络框架,scrapy使用了一种非阻塞(又名异步)的代码实现并发的,Scrapy之所以能实现异步,得益于twisted框架。twisted有事件队列,哪一个事件有活动,就会执行!Scrapy它集成高性能异步下载,队列,分布式,解析,持久化等。
1.五大核心组件
引擎(Scrapy)
框架核心,用来处理整个系统的数据流的流动, 触发事务(判断是何种数据流,然后再调用相应的方法)。也就是负责Spider、ItemPipeline、Downloader、Scheduler中间的通讯,信号、数据传递等,所以被称为框架的核心。
调度器(Scheduler)
用来接受引擎发过来的请求,并按照一定的方式进行整理排列,放到队列中,当引擎需要时,交还给引擎。可以想像成一个URL(抓取网页的网址或者说是链接)的优先队列, 由它来决定下一个要抓取的网址是什么, 同时去除重复的网址。
下载器(Downloader)
负责下载引擎发送的所有Requests请求,并将其获取到的Responses交还给Scrapy Engine(引擎),由引擎交给Spider来处理。Scrapy下载器是建立在twisted这个高效的异步模型上的。
爬虫(Spiders)
用户根据自己的需求,编写程序,用于从特定的网页中提取自己需要的信息,即所谓的实体(Item)。用户也可以从中提取出链接,让Scrapy继续抓取下一个页面。跟进的URL提交给引擎,再次进入Scheduler(调度器)。
项目管道(Pipeline)
负责处理爬虫提取出来的item,主要的功能是持久化实体、验证实体的有效性、清除不需要的信息。
2.工作流程
Scrapy中的数据流由引擎控制,其过程如下:
(1)用户编写爬虫主程序将需要下载的页面请求requests递交给引擎,引擎将请求转发给调度器;
(2)调度实现了优先级、去重等策略,调度从队列中取出一个请求,交给引擎转发给下载器(引擎和下载器中间有中间件,作用是对请求加工如:对requests添加代理、ua、cookie,response进行过滤等);
(3)下载器下载页面,将生成的响应通过下载器中间件发送到引擎;
(4) 爬虫主程序进行解析,这个时候解析函数将产生两类数据,一种是items、一种是链接(URL),其中requests按上面步骤交给调度器;items交给数据管道(数据管道实现数据的最终处理);
官方文档
英文版:https://docs.scrapy.org/en/latest/
http://doc.scrapy.org/en/master/
中文版:https://scrapy-chs.readthedocs.io/zh_CN/latest/intro/overview.html
https://www.osgeo.cn/scrapy/topics/architecture.html
二、安装及常用命令介绍
1. 安装
Linux:pip3 install scrapy
Windows:
a. pip3 install wheel
b. 下载twisted http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
c. shift右击进入下载目录,执行 pip3 install typed_ast-1.4.0-cp36-cp36m-win32.whl
d. pip3 install pywin32
e. pip3 install scrapy
2.scrapy基本命令行
(1)创建一个新的项目
scrapy startproject ProjectName
(2)生成爬虫
scrapy genspider +SpiderName+website
(3)运行(crawl) # -o output
scrapy crawl +SpiderName
scrapy crawl SpiderName -o file.json
scrapy crawl SpiderName-o file.csv
(4)检查spider文件是否有语法错误
scrapy check
(5)list返回项目所有spider名称
scrapy list
(6)测试电脑当前爬取速度性能:
scrapy bench
(7)scrapy runspider
scrapy runspider zufang_spider.py
(8)编辑spider文件:
scrapy edit <spider> 相当于打开vim模式,实际并不好用,在IDE中编辑更为合适。
(9)将网页内容下载下来,然后在终端打印当前返回的内容,相当于 request 和 urllib 方法:
scrapy fetch <url> (10)将网页内容保存下来,并在浏览器中打开当前网页内容,直观呈现要爬取网页的内容:
scrapy view <url> (11)进入终端。打开 scrapy 显示台,类似ipython,可以用来做测试:
scrapy shell [url]
(12)输出格式化内容:
scrapy parse <url> [options]
(13)返回系统设置信息:
scrapy settings [options]
如:
$ scrapy settings --get BOT_NAME
scrapybot
(14)显示scrapy版本:
scrapy version [-v]
后面加 -v 可以显示scrapy依赖库的版本
三、简单实例
以麦田租房信息爬取为例,网站http://bj.maitian.cn/zfall/PG1
1.创建项目
scrapy startproject houseinfo
生成项目结构:
scrapy.cfg 项目的主配置信息。(真正爬虫相关的配置信息在settings.py文件中)
items.py 设置数据存储模板,用于结构化数据,如:Django的Model
pipelines 数据持久化处理
settings.py 配置文件
spiders 爬虫目录
2.创建爬虫应用程序
cd houseinfo
scrapy genspider maitian maitian.com
然后就可以在spiders目录下看到我们的爬虫主程序
3.编写爬虫文件
步骤2执行完毕后,会在项目的spiders中生成一个应用名的py爬虫文件,文件源码如下:
# -*- coding: utf-8 -*-
import scrapy
class MaitianSpider(scrapy.Spider):
name = 'maitian' # 应用名称
allowed_domains = ['maitian.com'] #一般注释掉,允许爬取的域名(如果遇到非该域名的url则爬取不到数据)
start_urls = ['http://maitian.com/'] #起始爬取的url列表,该列表中存在的url,都会被parse进行请求的发送
#解析函数
def parse(self, response):
pass
我们可以在此基础上,根据需求进行编写
# -*- coding: utf-8 -*-
import scrapy
class MaitianSpider(scrapy.Spider):
name = 'maitian'
start_urls = ['http://bj.maitian.cn/zfall/PG100']
#解析函数
def parse(self, response):
li_list = response.xpath('//div[@class="list_wrap"]/ul/li')
results = []
for li in li_list:
title = li.xpath('./div[2]/h1/a/text()').extract_first().strip()
price = li.xpath('./div[2]/div/ol/strong/span/text()').extract_first().strip()
square = li.xpath('./div[2]/p[1]/span[1]/text()').extract_first().replace('㎡','') # 将面积的单位去掉
area = li.xpath('./div[2]/p[2]/span/text()[2]').extract_first().strip().split('\xa0')[0] # 以空格分隔
adress = li.xpath('./div[2]/p[2]/span/text()[2]').extract_first().strip().split('\xa0')[2]
dict = {
"标题":title,
"月租金":price,
"面积":square,
"区域":area,
"地址":adress
}
results.append(dict)
print(title,price,square,area,adress)
return results
须知:
- xpath为scrapy中的解析方式
- xpath函数返回的为列表,列表中存放的数据为Selector类型数据。解析到的内容被封装在Selector对象中,需要调用extract()函数将解析的内容从Selec****t****or中取出。
- 如果可以保证xpath返回的列表中只有一个列表元素,则可以使用extract_first(),****否则必须使用extract()
两者等同,都是将列表中的内容提取出来
title = li.xpath('./div[2]/h1/a/text()').extract_first().strip()
title = li.xpath('./div[2]/h1/a/text()')[0].extract().strip()
4. 设置修改settings.py配置文件相关配置:
# 伪装请求载体身份
USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36'
# 可以忽略或者不遵守robots协议
ROBOTSTXT_OBEY = False
5.执行爬虫程序:scrapy crawl maitain
爬取全站数据,也就是全部页码数据。本例中,总共100页,观察页面之间的共性,构造通用url
方式一:通过占位符,构造通用url
import scrapy
class MaitianSpider(scrapy.Spider):
name = 'maitian'
start_urls = ['http://bj.maitian.cn/zfall/PG{}'.format(page) for page in range(1,4)] #注意写法
#解析函数
def parse(self, response):
li_list = response.xpath('//div[@class="list_wrap"]/ul/li')
results = []
for li in li_list:
title = li.xpath('./div[2]/h1/a/text()').extract_first().strip()
price = li.xpath('./div[2]/div/ol/strong/span/text()').extract_first().strip()
square = li.xpath('./div[2]/p[1]/span[1]/text()').extract_first().replace('㎡','')
# 也可以通过正则匹配提取出来
area = li.xpath('./div[2]/p[2]/span/text()[2]')..re(r'昌平|朝阳|东城|大兴|丰台|海淀|石景山|顺义|通州|西城')[0]
adress = li.xpath('./div[2]/p[2]/span/text()[2]').extract_first().strip().split('\xa0')[2]
dict = {
"标题":title,
"月租金":price,
"面积":square,
"区域":area,
"地址":adress
}
results.append(dict)
return results
如果碰到一个表达式不能包含所有情况的项目,解决方式是先分别写表达式,最后通过列表相加,将所有url合并成一个url列表,例如
start_urls = ['http://www.guokr.com/ask/hottest/?page={}'.format(n) for n in range(1, 8)] + [
'http://www.guokr.com/ask/highlight/?page={}'.format(m) for m in range(1, 101)]
方式二:通过重写start_requests方法,获取所有的起始url。(不用写start_urls
)
import scrapy
class MaitianSpider(scrapy.Spider):
name = 'maitian'
def start_requests(self):
pages=[]
for page in range(90,100):
url='http://bj.maitian.cn/zfall/PG{}'.format(page)
page=scrapy.Request(url)
pages.append(page)
return pages
#解析函数
def parse(self, response):
li_list = response.xpath('//div[@class="list_wrap"]/ul/li')
results = []
for li in li_list:
title = li.xpath('./div[2]/h1/a/text()').extract_first().strip(),
price = li.xpath('./div[2]/div/ol/strong/span/text()').extract_first().strip(),
square = li.xpath('./div[2]/p[1]/span[1]/text()').extract_first().replace('㎡',''),
area = li.xpath('./div[2]/p[2]/span/text()[2]').re(r'昌平|朝阳|东城|大兴|丰台|海淀|石景山|顺义|通州|西城')[0],
adress = li.xpath('./div[2]/p[2]/span/text()[2]').extract_first().strip().split('\xa0')[2]
dict = {
"标题":title,
"月租金":price,
"面积":square,
"区域":area,
"地址":adress
}
results.append(dict)
return results
四、数据持久化存储
- 基于终端指令的持久化存储
- 基于管道的持久化存储
只要是数据持久化存储,parse方法必须有返回值(也就是return后的内容)
1. 基于终端指令的持久化存储
执行输出指定格式进行存储:将爬取到的数据写入不同格式的文件中进行存储,windows终端不能使用txt格式
scrapy crawl 爬虫名称 -o xxx.json
scrapy crawl 爬虫名称 -o xxx.xml
scrapy crawl 爬虫名称 -o xxx.csv
以麦田为例,spider中的代码不变,将返回值写到qiubai.csv中。本地没有,就会自己创建一个。本地有就会追加
scrapy crawl maitian -o maitian.csv
就会在项目目录下看到,生成的文件
查看文件内容
2.基于管道的持久化存储
scrapy框架中已经为我们专门集成好了高效、便捷的持久化操作功能,我们直接使用即可。要想使用scrapy的持久化操作功能,我们首先来认识如下两个文件:
- items.py:数据结构模板文件。定义数据属性。
- pipelines.py:管道文件。接收数据(items),进行持久化操作。
持久化流程:
①爬虫文件爬取到数据解析后,需要将数据封装到items对象中。
②使用yield关键字将items对象提交给pipelines管道,进行持久化操作。
③在管道文件中的process_item方法中接收爬虫文件提交过来的item对象,然后编写持久化存储的代码,将item对象中存储的数据进行持久化存储(在管道的process_item方法中执行io操作,进行持久化存储)
④settings.py配置文件中开启管道
2.1保存到本地的持久化存储
爬虫文件:maitian.py
import scrapy
from houseinfo.items import HouseinfoItem # 将item导入
class MaitianSpider(scrapy.Spider):
name = 'maitian'
start_urls = ['http://bj.maitian.cn/zfall/PG100']
#解析函数
def parse(self, response):
li_list = response.xpath('//div[@class="list_wrap"]/ul/li')
for li in li_list:
item = HouseinfoItem(
title = li.xpath('./div[2]/h1/a/text()').extract_first().strip(),
price = li.xpath('./div[2]/div/ol/strong/span/text()').extract_first().strip(),
square = li.xpath('./div[2]/p[1]/span[1]/text()').extract_first().replace('㎡',''),
area = li.xpath('./div[2]/p[2]/span/text()[2]').extract_first().strip().split('\xa0')[0],
adress = li.xpath('./div[2]/p[2]/span/text()[2]').extract_first().strip().split('\xa0')[2]
)
yield item # 提交给管道,然后管道定义存储方式
items文件:items.py
import scrapy
class HouseinfoItem(scrapy.Item):
title = scrapy.Field() #存储标题,里面可以存储任意类型的数据
price = scrapy.Field()
square = scrapy.Field()
area = scrapy.Field()
adress = scrapy.Field()
管道文件:pipelines.py
class HouseinfoPipeline(object):
def __init__(self):
self.file = None
#开始爬虫时,执行一次
def open_spider(self,spider):
self.file = open('maitian.csv','a',encoding='utf-8') # 选用了追加模式
self.file.write(",".join(["标题","月租金","面积","区域","地址","\n"]))
print("开始爬虫")
# 因为该方法会被执行调用多次,所以文件的开启和关闭操作写在了另外两个只会各自执行一次的方法中。
def process_item(self, item, spider):
content = [item["title"], item["price"], item["square"], item["area"], item["adress"], "\n"]
self.file.write(",".join(content))
return item
# 结束爬虫时,执行一次
def close_spider(self,spider):
self.file.close()
print("结束爬虫")
配置文件:settings.py
#伪装请求载体身份
USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36'
#可以忽略或者不遵守robots协议
ROBOTSTXT_OBEY = False
#开启管道
ITEM_PIPELINES = {
'houseinfo.pipelines.HouseinfoPipeline': 300, #数值300表示为优先级,值越小优先级越高
}
五、爬取多级页面
爬取多级页面,会遇到2个问题:
问题1:如何对下一层级页面发送请求?
答:在每一个解析函数的末尾,通过Request方法对下一层级的页面手动发起请求
# 先提取二级页面url,再对二级页面发送请求。多级页面以此类推
def parse(self, response):
next_url = response.xpath('//div[2]/h2/a/@href').extract()[0] # 提取二级页面url
yield scrapy.Request(url=next_url, callback=self.next_parse) # 对二级页面发送请求,注意要用yield,回调函数不带括号
问题2:解析的数据不在同一张页面中,最终如何将数据传递
答:涉及到请求传参,可以在对下一层级页面发送请求的时候,通过meta参数进行数据传递,meta字典就会传递给回调函数的response参数。下一级的解析函数通过response获取item(先通过 response.meta返回接收到的meta字典,再获得item字典)
# 通过meta参数进行Request的数据传递,meta字典就会传递给回调函数的response参数
def parse(self, response):
item = Item() # 实例化item对象
Item["field1"] = response.xpath('expression1').extract()[0] # 列表中只有一个元素
Item["field2"] = response.xpath('expression2').extract() # 列表
next_url = response.xpath('expression3').extract()[0] # 提取二级页面url
# meta参数:请求传参.通过meta参数进行Request的数据传递,meta字典就会传递给回调函数的response参数
yield scrapy.Request(url=next_url, callback=self.next_parse,meta={'item':item}) # 对二级页面发送请求
def next_parse(self,response):
# 通过response获取item. 先通过 response.meta返回接收到的meta字典,再获得item字典
item = response.meta['item']
item['field'] = response.xpath('expression').extract_first()
yield item #提交给管道
案例1:麦田,对所有页码发送请求。不推荐将每一个页码对应的url存放到爬虫文件的起始url列表(start_urls)中。这里我们使用Request方法手动发起请求。
# -*- coding: utf-8 -*-
import scrapy
from houseinfo.items import HouseinfoItem # 将item导入
class MaitianSpider(scrapy.Spider):
name = 'maitian'
start_urls = ['http://bj.maitian.cn/zfall/PG1']
#爬取多页
page = 1
url = 'http://bj.maitian.cn/zfall/PG%d'
#解析函数
def parse(self, response):
li_list = response.xpath('//div[@class="list_wrap"]/ul/li')
for li in li_list:
item = HouseinfoItem(
title = li.xpath('./div[2]/h1/a/text()').extract_first().strip(),
price = li.xpath('./div[2]/div/ol/strong/span/text()').extract_first().strip(),
square = li.xpath('./div[2]/p[1]/span[1]/text()').extract_first().replace('㎡',''),
area = li.xpath('./div[2]/p[2]/span/text()[2]').re(r'昌平|朝阳|东城|大兴|丰台|海淀|石景山|顺义|通州|西城')[0], # 也可以通过正则匹配提取出来
adress = li.xpath('./div[2]/p[2]/span/text()[2]').extract_first().strip().split('\xa0')[2]
)
['http://bj.maitian.cn/zfall/PG{}'.format(page) for page in range(1, 4)]
yield item # 提交给管道,然后管道定义存储方式
if self.page < 4:
self.page += 1
new_url = format(self.url%self.page) # 这里的%是拼接的意思
yield scrapy.Request(url=new_url,callback=self.parse) # 手动发起一个请求,注意一定要写yield
案例2:这个案例比较好的一点是,parse函数,既有对下一页的回调,又有对详情页的回调
import scrapy
class QuotesSpider(scrapy.Spider):
name = 'quotes_2_3'
start_urls = [
'http://quotes.toscrape.com',
]
allowed_domains = [
'toscrape.com',
]
def parse(self,response):
for quote in response.css('div.quote'):
yield{
'quote': quote.css('span.text::text').extract_first(),
'author': quote.css('small.author::text').extract_first(),
'tags': quote.css('div.tags a.tag::text').extract(),
}
author_page = response.css('small.author+a::attr(href)').extract_first()
authro_full_url = response.urljoin(author_page)
yield scrapy.Request(authro_full_url, callback=self.parse_author) # 对详情页发送请求,回调详情页的解析函数
next_page = response.css('li.next a::attr("href")').extract_first() # 通过css选择器定位到下一页
if next_page is not None:
next_full_url = response.urljoin(next_page)
yield scrapy.Request(next_full_url, callback=self.parse) # 对下一页发送请求,回调自己的解析函数
def parse_author(self,response):
yield{
'author': response.css('.author-title::text').extract_first(),
'author_born_date': response.css('.author-born-date::text').extract_first(),
'author_born_location': response.css('.author-born-location::text').extract_first(),
'authro_description': response.css('.author-born-location::text').extract_first(),
案例3:爬取www.id97.com电影网,将一级页面中的电影名称,类型,评分,二级页面中的上映时间,导演,片长进行爬取。(多级页面+传参)
# -*- coding: utf-8 -*-
import scrapy
from moviePro.items import MovieproItem
class MovieSpider(scrapy.Spider):
name = 'movie'
allowed_domains = ['www.id97.com']
start_urls = ['http://www.id97.com/']
def parse(self, response):
div_list = response.xpath('//div[@class="col-xs-1-5 movie-item"]')
for div in div_list:
item = MovieproItem() item['name'] = div.xpath('.//h1/a/text()').extract_first()
item['score'] = div.xpath('.//h1/em/text()').extract_first()
item['kind'] = div.xpath('.//div[@class="otherinfo"]').xpath('string(.)').extract_first()
item['detail_url'] = div.xpath('./div/a/@href').extract_first()
#meta参数:请求传参.通过meta参数进行Request的数据传递,meta字典就会传递给回调函数的response参数
yield scrapy.Request(url=item['detail_url'],callback=self.parse_detail,meta={'item':item})
def parse_detail(self,response):
#通过response获取item. 先通过 response.meta返回接收到的meta字典,再获得item字典
item = response.meta['item']
item['actor'] = response.xpath('//div[@class="row"]//table/tr[1]/a/text()').extract_first()
item['time'] = response.xpath('//div[@class="row"]//table/tr[7]/td[2]/text()').extract_first()
item['long'] = response.xpath('//div[@class="row"]//table/tr[8]/td[2]/text()').extract_first()
yield item #提交item到管道
以上案例都只贴出了爬虫主程序脚本,因篇幅原因,所以item、pipeline和settings等脚本未贴出,可参考上面案例进行编写。
六、Scrapy发送post请求
问题:在之前代码中,我们从来没有手动的对start_urls列表中存储的起始url进行过请求的发送,但是起始url的确是进行了请求的发送,那这是如何实现的呢?
解答:其实是因为爬虫文件中的爬虫类继承到了Spider父类中的start_requests(self)这个方法,该方法就可以对start_urls列表中的url发起请求:
def start_requests(self):
for u in self.start_urls:
yield scrapy.Request(url=u,callback=self.parse)
注意:****该方法默认的实现,是对起始的url发起get请求,如果想发起post请求,则需要子类重写该方法。不过,****一般情况下不用scrapy发post请求,用request模块。
例:爬取百度翻译
*- coding: utf-8 -*-
import scrapy
class PostSpider(scrapy.Spider):
name = 'post'
# allowed_domains = ['www.xxx.com']
start_urls = ['https://fanyi.baidu.com/sug']
def start_requests(self):
data = { # post请求参数
'kw':'dog'
}
for url in self.start_urls:
yield scrapy.FormRequest(url=url,formdata=data,callback=self.parse) # 发送post请求
def parse(self, response):
print(response.text)
七、设置日志等级
在使用scrapy crawl spiderFileName运行程序时,在终端里打印输出的就是scrapy的日志信息。
日志信息的种类:
ERROR : 一般错误
WARNING : 警告
INFO : 一般的信息
DEBUG : 调试信息设置日志信息指定输出:
在settings.py配置文件中,加入
LOG_LEVEL = ‘指定日志信息种类’即可。
LOG_FILE = 'log.txt'则表示将日志信息写入到指定文件中进行存储。
其他常用设置:
BOT_NAME
默认:“scrapybot”,使用startproject命令创建项目时,其被自动赋值
CONCURRENT_ITEMS
默认为100,Item Process(即Item Pipeline)同时处理(每个response的)item时最大值
CONCURRENT_REQUEST
默认为16,scrapy downloader并发请求(concurrent requests)的最大值
LOG_ENABLED
默认为True,是否启用logging
DEFAULT_REQUEST_HEADERS
默认如下:{'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'en',}
scrapy http request使用的默认header
LOG_ENCODING
默认utt-8,logging中使用的编码
LOG_LEVEL
默认“DEBUG”,log中最低级别,可选级别有:CRITICAL,ERROR,WARNING,DEBUG
USER_AGENT
默认:“Scrapy/VERSION(....)”,爬取的默认User-Agent,除非被覆盖
COOKIES_ENABLED=False,禁用cookies
八、同时运行多个爬虫
实际开发中,通常在同一个项目里会有多个爬虫,多个爬虫的时候是怎么将他们运行起来呢?
运行单个爬虫
import sys
from scrapy.cmdline import execute
if __name__ == '__main__':
execute(["scrapy","crawl","maitian","--nolog"])
然后运行py文件即可运行名为‘maitian‘的爬虫
同时运行多个爬虫
步骤如下:
- 在spiders同级创建任意目录,如:commands
- 在其中创建 crawlall.py 文件 (此处文件名就是自定义的命令)
- 在settings.py 中添加配置 COMMANDS_MODULE = '项目名称.目录名称'
- 在项目目录执行命令:scrapy crawlall
crawlall.py代码
from scrapy.commands import ScrapyCommand
from scrapy.utils.project import get_project_settings
class Command(ScrapyCommand):
requires_project = True
def syntax(self):
return '[options]'
def short_desc(self):
return 'Runs all of the spiders'
def run(self, args, opts):
spider_list = self.crawler_process.spiders.list()
for name in spider_list:
self.crawler_process.crawl(name, **opts.__dict__)
self.crawler_process.start()