1.scrapy环境安装及一些基础指令
1.1scrapy环境安装
scrapy作为Python爬虫中的一个明星框架,其拥有的强大功能和为我们提供的便捷高效的方法使多少人趋之若鹜。现在,就来对scrapy有一个简单认识和初步使用。注:对于框架的学习,应该把重心放在怎么用而不是为什么,至于为什么的问题可以在学习完成后自行翻阅scrapy源码。
首先,是scrapy环境的安装。注意,这里分为两种情况,如果你的系统是Mac或者是Linux,就可以直接在终端输入如下指令
pip install scrapy
即可完成安装。
但是,如果是Windows系统就会比较麻烦一点。首先,先输入如下指令安装wheel
pip install wheel
然后去 http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted 下载twisted包
接着找到下载的文件夹,在文件夹中的空白位置右键点击 "在终端中打开" ,如下图所示。
输入指令
pip install Twisted-17.1.0-cp36-cp36m-win_amd64.whl
其中 Twisted-17.1.0-cp36-cp36m-win_amd64.whl 为自己刚刚下载的具体twisted名称。
注意:如果安装不成功,可能是版本不兼容的问题,可以下载比Python版本低的包。像笔者是3.8的python版本,最开始下3.8的包未安装成功。改下3.6的包就成功了。
最后再输入指令
pip install pywin32
pip install scrapy
就安装成功了。可以通过再终端输入scrapy指令,没有报错即表示安装成功。
1.2基础指令
创建一个工程
首先输入
scrapy startproject 工程名称
然后输入
cd 工程名称
接着输入
scrapy genspider 爬虫名称 网址
注意:这个指令是在spiders子目录中创建一个爬虫文件,网址可以随便写,因为在后面可以改
到此为止,一个工程就创建成功了。如下图
2.scrapy中的一些基础设置和scrapy初使用
2.1基础设置的改动
首先是在settings.py文件中,找一个地方做如下改动
添加一个LOG_LEVEL,这个作用是当我们在打印一些信息的时候,一些日志信息也会不合时宜的被打印出来,影响效果。而这个加上之后,就只会当程序报错的时候才会打印出来,其他时候只会打印我们想看到的数据。
添加一个USER_AGENT,这个是对我们的爬虫进行一个UA伪装,至于USER_AGENT的值,大家可以用自己的电脑。具体查询方式如下
随便访问一个网址,然后在页面空白处右键找到 "检查" 点击,然后按下图操作
找到ROBOTSTXT_OBEY,对其做如下改动
这里如果是True,就代表着遵守robot协议,结果就是咱们啥也爬不到。但是我们在根本上还是要遵守robot协议,只是这里为了scrapy学习暂时改动。
接着是在刚刚我们创建的爬虫文件当中
这是未作任何改动的样子,可以看到在报警告,现在做如下改动即可解决
2.2爬虫文件中一些属性及方法的解释和scrapy初体验
现在我们对刚刚上图中DemoSpider这个类中的那三个属性和一个方法做出解释
第一个是 "name" 。这个是爬虫文件的名称,是爬虫源文件的一个唯一标识。
第二个是 "allowed_domains" 。这是允许的域名,用来限定start_urls列表中哪些url可以进行请求发送。不过用起来不方便,一般直接注释掉,如下图
第三个是 "start_urls" 。这是起始的url列表,该列表中存放的url会被scrapy自动进行请求发送,刚刚创建的时候为什么说网址随便写,就是因为可以在这里改,如下图
第四个是 "parse" 这个方法。这个方法用作数据解析,response参数表示的就是请求成功后对应的响应对象,在里面写我们想做的操作。
基础的介绍完毕,就来初次感受感受scrapy吧!
刚开始就写一个简单的打印语句,如下图
然后注意:运行scrapy是在终端输入
scrapy crawl 爬虫名称
把我们两个response对象打印了出来。
3.scrapy当中的永久存储
3.1基于终端指令
对于这个场景,我们写了一个代码
import scrapy
class DemoSpider(scrapy.Spider):
name = "demo"
# allowed_domains = ["www.xxx.com"]
start_urls = ["https://www.qiushibaike.com/text/"]
def parse(self, response, *args, **kwargs):
# 解析作者的名称+段子的内容
div_list = response.xpath('//div[@id="content-left"]/div')
# 存储解析到的数据
all_data = []
for div in div_list:
# extrac_first()可以将对象中的字符串提取出来
author = div.xpath('./div[1]/a[2]/h2/text()').extract_first()
# extract()可以将其中所有字符串提取在一个列表中
content = div.xpath('./a[1]/div/span//text()').extract()
content = "".join(content)
dic = {
"author": author,
"content": content
}
all_data.append(dic)
return all_data
其中涉及一些xpath的知识。这里强调两个方法:如果我们定位到了一个标签,想获取这个标签当中的内容,可以用extract_first()把内容提取出来。如果我们想获取一个标签当中包括子标签的内容,比如刚刚代码中span中有许多子标签,这些子标签中都有我们想要的数据,就可以用extract()这个方法把所有子标签中的数据全部提取出来放在一个列表当中。再利用join方法把每个字符串拼接起来。
代码是把单个的作者和内容放在了一个字典中,然后把字典添加到总的列表中,再返回这个列表。这与基于终端指令存储的特点有关系。
下面来看基于终端指令存储的特点:
---要求:只可以将parse方法的返回值存储到本地的文件中
---注意:永久存储对应的文件类型只可以为:json,jsonlines,jl,csv,xml,marshal,pickle
---指令:scrapy crawl 爬虫名称 -o 文件路径
---好处:简洁高效便捷
---缺点:局限性较强,数据只可以储存到指定的文件中
运行工程如下图
注意:可能因时间问题url失效,大家可以换个自己熟悉的url,上述代码中的url好像已经不行了。。。
3.2基于管道
对于这个陌生的东西,我们直接上流程。
编码流程:
--- 数据解析
--- 在item类中定义相关的属性
--- 将解析的数据封装存储到item类型的对象
--- 将item类型的对象提交给管道进行永久存储的操作
--- 在管道类的process_item中要将其接受的item对象中存储的数据进行永久存储操作
--- 在配置文件中开启管道
我们直接上代码,然后一一讲解。
items.py
# Define here the models for your scraped items
#
# See documentation in:
# https://docs.scrapy.org/en/latest/topics/items.html
import scrapy
class DemoproItem(scrapy.Item):
# define the fields for your item here like:
author = scrapy.Field()
content = scrapy.Field()
# pass
demo.py
import scrapy
from firstBlood.DemoPro.DemoPro.items import DemoproItem
class DemoSpider(scrapy.Spider):
name = "demo"
# allowed_domains = ["www.xxx.com"]
start_urls = ["https://www.qiushibaike.com/text/"]
def parse(self, response, *args, **kwargs):
# 解析作者的名称+段子的内容
div_list = response.xpath('//div[@id="content-left"]/div')
for div in div_list:
# extrac_first()可以将对象中的字符串提取出来
author = div.xpath('./div[1]/a[2]/h2/text()').extract_first()
# extract()可以将其中所有字符串提取在一个列表中
content = div.xpath('./a[1]/div/span//text()').extract()
content = "".join(content)
item = DemoproItem()
item['author'] = author
item['content'] = content
# 将item提交给了管道
yield item
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 DemoproPipeline:
# 文件对象
fp = None
# 重写父类的一个方法,该方法只在开始爬虫的时候被调用一次
def open_spider(self, spider):
print("开始爬虫...")
self.fp = open('./qiubai.txt', 'w', encoding='utf-8')
# 专门用来处理item类型对象,可以接收爬虫文件提交过来的item对象,每接收到一个item就会被调用一次
def process_item(self, item, spider):
author = item['author']
content = item['content']
self.fp.write(author + ':' + content + '\n')
return item
# 重写父类的一个方法,该方法只在结束爬虫的时候被调用一次
def close_spider(self, spider):
print("结束爬虫...")
self.fp.close()
settings.py
找到这个ITEM_PIPELINES,然后把注释取消。
这时候就大功告成。接下来,我们来逐个讲解。
首先是item.py文件,在其中已经有了一个类,将我们想存储的数据按照上述代码的格式写进去,这里注意,如果根据实际情况需要两个item类,就自己在后面照着写一个item类,不过名字别一样。
然后就是demo.py,也就是爬虫文件。在parse方法中,我们创建这个item类对象,将我们提取出来的数据按照上述代码格式存储到item对象中。这里注意:像刚刚说的,如果有多个item类,根据数据的不同创建不同的item类对象来存储就是。在存储好后,用
yield item
提交到管道。
在pipelines.py文件中,常用的三个方法的作用我已经注释在代码中,这里只是选择了用文件存储,并不是一定要三个方法都用上。最主要的就是process_item方法。大家也可以选择利用pymysql库将数据存储在MySQL当中。
最后在settings.py中找到被注释的代码,取消注释开启管道即可。注意:管道名称后的数字代表了优先级。当我们有不止一个管道的时候,提交的item会根据这个值来判断先被提交到哪个管道,此管道用完item后再通过process_item方法最后的return item将item提交到下一个优先级管道。这里值越小,优先级越高。
4.基于scrapy.Spider的全站爬取
4.1手动请求发送
各位设想一下,如果现在我们有一个需求:去爬取一个网站全部的数据,比如爬取新闻网的所有新闻或者是照片网的所有照片,我们会怎么做?
有一个很简单就可以想到的方法:把我们要访问的url全部放在start_urls里面,然后一个一个去请求访问。但是各位觉得这个方法好吗?肯定是不好的,就这个麻烦程度就不想去弄。那么这时候就有另外一个方法浮现出来:我们可以从一个网页中爬取到其他将要请求访问网页的url,然后用一个循环,对这些url来进行访问或者用一个循环对要访问的url进行请求发送。这里就涉及到一个技术:手动请求发送。
废话少说,直接上代码。
import scrapy
class DemoSpider(scrapy.Spider):
name = "demo"
# allowed_domains = ["www.xxx.com"]
start_urls = ["http://www.521609.com/meinvxiaohua/"]
# 页码
page_num = 2
# 生成一个通用的url模板
url = f'http://www.521609.com/meinvxiaohua/list12{page_num}.html'
def parse(self, response, *args, **kwargs):
li_list = response.xpath('//*[@id="content"]/div[2]/div[2]/ul/li')
for li in li_list:
img_name = li.xpath('./a[2]/b/text() | ./a[2]/text()').extract_first()
print(img_name)
if self.page_num <= 11:
self.page_num += 1
# 手动请求发送,callback回调函数是专门作用于数据解析
yield scrapy.Request(url=self.url, callback=self.parse, )
现在来逐一讲解。注意:这个案例中的url失效,只是为了演示用法。
首先,一般全站爬取都是有页码的,我们可以先对页码的url找到规律,一般都是一个小地方在变化,这时候可以把不变的部分提取出来作为模板,通过循环把变化的部分和模板组合起来,这样就可以得到我们想要的url。
上面代码很简单,最主要的就是最后那里。如果页码满足要求,就对一个新的url手动发起请求,格式大家记住就行,主要对几个参数进行解释。url不用多说,就是对这个参数的值发送请求的。callback是回调函数,可以理解为发送请求得到了一个响应对象后去调用的函数,其作用就像自动发送请求后的parse一样,对返回的响应对象做数据解析。注意:这里回调函数不一定非得是parse,也可以重新写一个函数,并且在传参的时候没有括号。
4.2请求传参
这里又来设想一个场景:假如我们要爬取解析的数据不在同一个界面怎么办?比如一个新闻网站,新闻标题得点进去才能看到具体内容,如果我们盲目提取后存储,就会出现标题和内容对不上的情况。这里就需要用到另一项技术:请求传参。就是在对一个url发请求的时候,传递一些信息,这里就可以在对新闻标题具体内容的url传参的时候把新闻标题传进去,就实现了标题和内容的一致性。
还是直接上代码
items.py
# Define here the models for your scraped items
#
# See documentation in:
# https://docs.scrapy.org/en/latest/topics/items.html
import scrapy
class DemoproItem(scrapy.Item):
# define the fields for your item here like:
news_title = scrapy.Field()
news_detail = scrapy.Field()
# pass
demo.py
import scrapy
from DemoPro.items import DemoproItem
class DemoSpider(scrapy.Spider):
name = "demo"
# allowed_domains = ["www.xxx.com"]
start_urls = ["https://www.chinanews.com.cn/"]
def parse(self, response, *args, **kwargs):
li_list = response.xpath('//*[@id="YwNes"]/div[2]/div[1]/div[2]/ul/li')
a = 0
for li in li_list:
if a < 8:
a += 1
else:
break
news_title = li.xpath('./a/text()').extract_first()
url = "https://www.chinanews.com.cn" + li.xpath('./a/@href').extract_first()
yield scrapy.Request(url=url, callback=self.parse_detail, meta={"news_title": news_title})
def parse_detail(self, response):
news_title = response.meta['news_title']
news_detail = response.xpath('//*[@id="cont_1_1_2"]/div[2]/div[4]/div[2]//p/text()').extract()
news_detail = "\n".join(news_detail)
item = DemoproItem()
item['news_title'] = news_title
item['news_detail'] = news_detail
yield item
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 DemoproPipeline:
# 专门用来处理item类型对象,可以接收爬虫文件提交过来的item对象,每接收到一个item就会被调用一次
def process_item(self, item, spider):
news_title = item['news_title']
news_detail = item['news_detail']
print(news_title)
print(news_detail)
print("=" * 30)
return item
开始讲解。这个案例是有效的,爬取中国新闻网的一些新闻题目和具体内容,题目和内容不在同一界面。注意:导包的时候记得修改上面的代码。
首先,item类写了两个要存储的数据,一个名称,一个描述。这很简单不再赘述。
然后,爬虫文件中定位到了每一个新闻题目(这里做案例就只爬取了一部分,故代码中有一个a变量来控制数量)和下一个界面的url,这时候手动对这个url请求发送,比之前多了一个参数meta,这是一个字典。把想传的数据按照上面代码格式写好。这次的回调函数就是另外写的了。回调函数中获取传过来的参数格式也按照代码来,在获取到新闻题目后,通过item存储,最后提交item给管道。
管道这里并没有永久存储,只是打印了一下,这里看各位的喜好。
5.scrapy的五大核心组件
这里不多说什么,下图讲的很清楚。
6.scrapy中的图片数据爬取
6.1传统方法
我们知道,对于字符串的永久存储和图片的永久存储是不一样的。这里补充两个方法,以前的代码全部是在用xpath进行解析,而如果我们想要所有页面源码呢?
可以使用response.text获取到整个页面的源码,但是如果我们访问的url是图片或者是视频呢?可以使用response.body获取二进制数据。
所以在scrapy中永久存储图片的传统方法就是基于xpath解析出图片标签的src的属性值,然后单独对图片地址发起请求获取图片二进制类型的数据。
最后永久存储在本地文件中即可。
这里就不用代码演示了,主要方法和逻辑已经说明。
6.2基于ImagesPipeline的图片数据爬取
管道可以存储我们的字符串数据,当然也可以存储图片数据。我们只需要将img标签中的src的属性值进行解析,提交到管道,管道就会对图片的src进行请求发送获取图片的二进制数据。
这里直接上使用流程:
--- 数据解析(图片的地址)注:有可能有伪属性,不一定是src
--- 将存储图片地址的item提交到制定的管道类
--- 在管道文件中自定制一个基于ImagesPipeLine的一个管道类
- get_media_request
- file_path
- item_completed
--- 在配置文件中
- 指定图片存储的目录:IMAGES_STORE = 自己决定
- 指定开启的管道:自定制的管道类
下面直接上代码。注:后面的代码可能只展示关键部分,一些不重要的文件内容将不再展示和解释。
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
from scrapy.pipelines.images import ImagesPipeline
import scrapy
class DemoproPipeline:
# 专门用来处理item类型对象,可以接收爬虫文件提交过来的item对象,每接收到一个item就会被调用一次
def process_item(self, item, spider):
news_title = item['news_title']
news_detail = item['news_detail']
print(news_title)
print(news_detail)
print("=" * 30)
return item
class imgsPipeLine(ImagesPipeline):
# 根据图片地址进行图片数据的请求
def get_media_requests(self, item, info):
yield scrapy.Request(url=item['src'])
# 指定图片存储的路径
def file_path(self, request, response=None, info=None, *, item=None):
img_name = request.url.split('/')[-1]
return img_name
def item_completed(self, results, item, info):
# 返回给下一个即将被执行的管道类
return item
settings.py
新添一个这个,目录自己指定
将开启的管道换成刚刚写的。注意:管道并不是只能开启一个,只是这里对之前那个无需使用。
这里具体的爬虫文件就不再展示了,都是大差不差的解析。
上面代码我们主要看第二个类,第一个是4.2请求传参的管道。第二个类中主要注意三个方法,而三个方法的解释我也在上述代码中的注释中说明了。这里解释一下为什么file_path那么写,我们知道图片地址如果按照 '/' 被执行split方法的话,返回的列表最后一个值是带有后缀的,我们便可以用这个值来充当图片的名称。而request.url是指从请求对象中拿到请求的url属性。
这样,永久存储的图片就被存储到我们在配置文件中指定的目录了。
7.中间件的使用
这里我们只讲下载中间件,也就是上面五大核心组件图中的
另外一个爬虫中间件这里不做讲解。
我们可以看到,下载中间件在引擎和下载器中间,那么我们就可以想到它的作用了:批量拦截到整个工程中所有的请求和响应。
这里我们分位拦截请求和拦截响应两个方面来讲解。
7.1拦截请求
那么在请求发出去之前我们把它拦截下来要干什么呢?各位想过这个问题没有,我们在配置文件中直接把UA写死了,也就是说,从咱们发出去的请求,UA全部是一样的,这样对于爬虫本身是非常不利的,所以我们要让我们发出去的请求的UA是变化的。这里我想各位已经猜到了:把请求拦截下来,然后随机给它加上一个UA。那随机的UA怎么解决呢?这里可以用UA池。
说到了UA,自然就会联想到IP。我们也可以把请求的IP换成其他的,和上面的逻辑差不多。
直接上代码
middlewares.py
# Define here the models for your spider middleware
#
# See documentation in:
# https://docs.scrapy.org/en/latest/topics/spider-middleware.html
from scrapy import signals
import random
# useful for handling different item types with a single interface
from itemadapter import is_item, ItemAdapter
class DemoproSpiderMiddleware:
# Not all methods need to be defined. If a method is not defined,
# scrapy acts as if the spider middleware does not modify the
# passed objects.
@classmethod
def from_crawler(cls, crawler):
# This method is used by Scrapy to create your spiders.
s = cls()
crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
return s
def process_spider_input(self, response, spider):
# Called for each response that goes through the spider
# middleware and into the spider.
# Should return None or raise an exception.
return None
def process_spider_output(self, response, result, spider):
# Called with the results returned from the Spider, after
# it has processed the response.
# Must return an iterable of Request, or item objects.
for i in result:
yield i
def process_spider_exception(self, response, exception, spider):
# Called when a spider or process_spider_input() method
# (from other spider middleware) raises an exception.
# Should return either None or an iterable of Request or item objects.
pass
def process_start_requests(self, start_requests, spider):
# Called with the start requests of the spider, and works
# similarly to the process_spider_output() method, except
# that it doesn’t have a response associated.
# Must return only requests (not items).
for r in start_requests:
yield r
def spider_opened(self, spider):
spider.logger.info("Spider opened: %s" % spider.name)
class DemoproDownloaderMiddleware:
# Not all methods need to be defined. If a method is not defined,
# scrapy acts as if the downloader middleware does not modify the
# passed objects.
user_agent_list = [
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 "
"(KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1",
"Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 "
"(KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 "
"(KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.6 "
"(KHTML, like Gecko) Chrome/20.0.1090.0 Safari/536.6",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.1 "
"(KHTML, like Gecko) Chrome/19.77.34.5 Safari/537.1",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.5 "
"(KHTML, like Gecko) Chrome/19.0.1084.9 Safari/536.5",
"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 "
"(KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 "
"(KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.3 "
"(KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 "
"(KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 "
"(KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 "
"(KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 "
"(KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 "
"(KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/536.3 "
"(KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 "
"(KHTML, like Gecko) Chrome/19.0.1061.0 Safari/536.3",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.24 "
"(KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.24 "
"(KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24"
]
PROXY_http = [
'126.120.102.124:80',
'101.208.131.169:56055',
]
PROXY_https = [
'20.83.49.90:900',
'91.182.12.212:3508',
]
@classmethod
# 拦截请求
def process_request(self, request, spider):
# Called for each request that goes through the downloader
# middleware.
# Must either:
# - return None: continue processing this request
# - or return a Response object
# - or return a Request object
# - or raise IgnoreRequest: process_exception() methods of
# installed downloader middleware will be called
# UA伪装
request.headers['User-Agent'] = random.choice(self.user_agent_list)
return None
# 拦截所有的响应
def process_response(self, request, response, spider):
# Called with the response returned from the downloader.
# Must either;
# - return a Response object
# - return a Request object
# - or raise IgnoreRequest
return response
# 拦截发生异常的请求
def process_exception(self, request, exception, spider):
# Called when a download handler or a process_request()
# (from other downloader middleware) raises an exception.
# Must either:
# - return None: continue processing this exception
# - return a Response object: stops process_exception() chain
# - return a Request object: stops process_exception() chain
# 代理
if request.url.split(':')[0] == 'http':
request.meta['proxy'] = 'http://' + random.choice(self.PROXY_http)
else:
request.meta['proxy'] = 'https://' + random.choice(self.PROXY_https)
# 将修正之后的请求对象进行重新的请求发送
return request
上面代码中第一个类可以看到是爬虫中间件,我们不去动它。
记得去把配置文件中的UA注释掉哟,还要去开启下载中间件。
然后看第二个类,其中它的第一个和最后一个方法的用处,我们在这里直接干掉了。接着在类中添加UA池和IP池,IP分为http和https,所以有两个。每个方法的用处在注释中,这里不过多讲解。更换UA的格式按照代码中写就行。更换IP是先进行判断,http和https使用不同的IP池。
这里,UA伪装和代理IP已经完成。
7.2拦截响应
那有人就会问,拦截请求还好理解,拦截响应能干什么,获取的东西都回来了,做什么不得无济于事?
这里你还别说,拦截响应还就是去篡改响应数据或者响应对象的。
大家想想,有些网站的数据是动态加载的,这时候我们获取数据只会空空如也。如果我们可以把返回的没有数据的响应对象篡改成有数据的响应对象,是不是程序就能正常执行了?
上代码
middlewares.py
# Define here the models for your spider middleware
#
# See documentation in:
# https://docs.scrapy.org/en/latest/topics/spider-middleware.html
from scrapy import signals
import random
from scrapy.http import HtmlResponse
# useful for handling different item types with a single interface
from itemadapter import is_item, ItemAdapter
class DemoproSpiderMiddleware:
# Not all methods need to be defined. If a method is not defined,
# scrapy acts as if the spider middleware does not modify the
# passed objects.
@classmethod
def from_crawler(cls, crawler):
# This method is used by Scrapy to create your spiders.
s = cls()
crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
return s
def process_spider_input(self, response, spider):
# Called for each response that goes through the spider
# middleware and into the spider.
# Should return None or raise an exception.
return None
def process_spider_output(self, response, result, spider):
# Called with the results returned from the Spider, after
# it has processed the response.
# Must return an iterable of Request, or item objects.
for i in result:
yield i
def process_spider_exception(self, response, exception, spider):
# Called when a spider or process_spider_input() method
# (from other spider middleware) raises an exception.
# Should return either None or an iterable of Request or item objects.
pass
def process_start_requests(self, start_requests, spider):
# Called with the start requests of the spider, and works
# similarly to the process_spider_output() method, except
# that it doesn’t have a response associated.
# Must return only requests (not items).
for r in start_requests:
yield r
def spider_opened(self, spider):
spider.logger.info("Spider opened: %s" % spider.name)
class DemoproDownloaderMiddleware:
# Not all methods need to be defined. If a method is not defined,
# scrapy acts as if the downloader middleware does not modify the
# passed objects.
user_agent_list = [
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 "
"(KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1",
"Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 "
"(KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 "
"(KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.6 "
"(KHTML, like Gecko) Chrome/20.0.1090.0 Safari/536.6",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.1 "
"(KHTML, like Gecko) Chrome/19.77.34.5 Safari/537.1",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.5 "
"(KHTML, like Gecko) Chrome/19.0.1084.9 Safari/536.5",
"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 "
"(KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 "
"(KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.3 "
"(KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 "
"(KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 "
"(KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 "
"(KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 "
"(KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 "
"(KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/536.3 "
"(KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 "
"(KHTML, like Gecko) Chrome/19.0.1061.0 Safari/536.3",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.24 "
"(KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.24 "
"(KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24"
]
PROXY_http = [
'126.120.102.124:80',
'101.208.131.169:56055',
]
PROXY_https = [
'20.83.49.90:900',
'91.182.12.212:3508',
]
@classmethod
# 拦截请求
def process_request(self, request, spider):
# Called for each request that goes through the downloader
# middleware.
# Must either:
# - return None: continue processing this request
# - or return a Response object
# - or return a Request object
# - or raise IgnoreRequest: process_exception() methods of
# installed downloader middleware will be called
# UA伪装
request.headers['User-Agent'] = random.choice(self.user_agent_list)
return None
# 拦截所有的响应
def process_response(self, request, response, spider):
# Called with the response returned from the downloader.
# Must either;
# - return a Response object
# - return a Request object
# - or raise IgnoreRequest
if request.url in spider.urls:
# 获取爬虫文件中的定义的浏览器对象
edge = spider.edge
edge.get(request.url)
page_text = edge.page_source
new_response = HtmlResponse(url=request.url, body=page_text, encoding='utf-8', request=request)
return new_response
else:
return response
# 拦截发生异常的请求
def process_exception(self, request, exception, spider):
# Called when a download handler or a process_request()
# (from other downloader middleware) raises an exception.
# Must either:
# - return None: continue processing this exception
# - return a Response object: stops process_exception() chain
# - return a Request object: stops process_exception() chain
# 代理
if request.url.split(':')[0] == 'http':
request.meta['proxy'] = 'http://' + random.choice(self.PROXY_http)
else:
request.meta['proxy'] = 'https://' + random.choice(self.PROXY_https)
# 将修正之后的请求对象进行重新的请求发送
return request
这里就展示这个关键文件,现在来一一讲解。
首先导了一个创建新响应对象的包。
其次我们要知道,篡改响应对象就是新建一个响应对象然后返回这个新的响应对象。我们想想:为什么新的响应对象就会有数据呢?因为我们可以用selenium去拿到这个有数据的响应对象啊。(selenium相关知识请见拙著:Python爬虫之selenium的基础使用)。所以我们爬虫文件中定义一个浏览器对象,在这里通过spider.edge去获取这个对象,这里的spider就是我们的爬虫文件,里面定义的属性可以像这个格式一样去获取。然后去访问url,再获取到有数据的网页源码page_text,最后我们创建一个新的响应对象,这里面有4个参数:url自然就是和响应对象所对应的请求对象的url;body就是网页源码(这么理解就行);encoding就是编码方式,我们选择utf-8;最后request是和响应对象所对应的请求对象。我们可以理解为,新的响应对象就是从旧的响应对象换了个body。然后返回新响应对象。
注意:代码中的spider.urls只是爬虫文件中定义的列表,旨在列表里面的url要篡改响应对象。各位也可以用自己的方法去判断什么url需要篡改响应对象。
对于不在这个列表中的url,就正常返回response就行了。
8.基于CrawlSpider的全站数据爬取
在前面我们已经讲解了一种方式的全站爬取,这里再介绍一种全新且更便捷的方式。那就是基于CrawlSpider类来实现。CrawlSpider类是Spider的一个子类。
这里直接上使用流程:
--- 创建一个工程
--- cd 工程
--- 创建爬虫文件(CrawlSpider):
-- scrapy genspider -t crawl 爬虫名称 www.xxx.com
-- 链接提取器:
- 作用:根据指定的规则(allow)进行指定链接的提取
-- 规则解析器:
- 作用:将链接提取器提取到的链接进行规则(callback)的解析
用一个代码来讲解,新创建的工程配置文件那些改动就不说了
爬虫文件
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
class CrawlSpider(CrawlSpider):
name = "Crawl"
# allowed_domains = ["www.xxx.com"]
start_urls = ["https://movie.douban.com/top250"]
# 链接提取器:根据指定规则(allow="正则")进行指定链接的提取
link = LinkExtractor(allow=r"start=\d+&filter=")
rules = (
Rule(link, callback="parse_item", follow=True),
)
def parse_item(self, response):
li_list = response.xpath('//*[@id="content"]/div/div[1]/ol/li')
for li in li_list:
name = li.xpath('./div/div[2]/div[1]/a/span[1]/text()').extract_first()
print(name)
逐一讲解。首先是link,本来是没有这个的,而是直接写在了Rule的构造函数中,我这里只是把它拿出来,看的舒服。link是一个对象,这个对象构造时要一个allow参数,这个参数就是一个正则表达式。我们想一下,一般全站爬取是不是都是由很多页,而这个allow就是帮我们去匹配那些页码的url,并且不会重复。注意:并不是一定要去匹配页码url,比如有一些url也需要去匹配,也可以用allow。然后在rules中加上Rule对象,把参数添上,其中第一个就是link;callback也是老熟人,就是回调函数;这个follow如果等于True的话,代表着可以将连接提取器 继续作用到 连接提取器提取到的链接 所对应的页面中,这里有点绕,好好理解,就是说所有页码的url都能获取到了。
然后是回调函数,没什么新奇的,定位标签,然后打印其中的字符串。
这个案例是去爬取豆瓣top250,各位可以试试,这个案例是有效的。
补充:其实如果还要去爬取每个电影的简介,可以再写一个link和回调函数,但这个link的follow就是False了。具体代码如下
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
class CrawlSpider(CrawlSpider):
name = "Crawl"
# allowed_domains = ["www.xxx.com"]
start_urls = ["https://movie.douban.com/top250"]
# 链接提取器:根据指定规则(allow="正则")进行指定链接的提取
link = LinkExtractor(allow=r"start=\d+&filter=")
link_detail = LinkExtractor(allow=r'subject/\d+/')
rules = (
Rule(link, callback="parse_item", follow=True),
Rule(link_detail, callback="parse_detail")
)
def parse_item(self, response):
li_list = response.xpath('//*[@id="content"]/div/div[1]/ol/li')
for li in li_list:
name = li.xpath('./div/div[2]/div[1]/a/span[1]/text()').extract_first()
print(name)
def parse_detail(self, response):
content = response.xpath('//*[@id="link-report-intra"]/span[1]/span//text()').extract()
content = "".join(content)
print(content)
具体打印出来效果不是很好,但是肯定实现了的。后面可以在对提取出来的信息再进行操作。
可以看到,用CrawlSpider来进行全站爬取是十分便捷的。
9.结尾
以上内容如有错误还请指正。感谢。