Scrapy是一个用Python实现的为了更加简洁的爬取网站数据、提取结构性数据而编写的多种方法的集合。用来抓取网页内容以及各种图片,非常之方便。
功能:高性能的持久化存储,异步的数据下载,高性能的数据解析,分布式。(通常不对动态加载数据进行爬取)
scrapy框架中也可以使用requests哦,不过不建议
创建工作环境
- 在终端依次输入:
- 1. mkdir scrapy_project #创建名为scrapy_project的文件夹以便创建环境
-
- 2.cd scrapy_project #进入到文件夹下
-
- 3. scrapy startproject firstscrapy #创建一个工程命名为firstscrapy
-
- 4.cd scrapy_project #进入创建好的工程文件
-
- 5.scrapy genspider first_one www.baidu.com
- #在spiders子目录中创建一个python爬虫文件命名为firstscrapy,后面是一个目标网址,可以随意写,可以改
-
- 然后就可以通过编写程序运行了
- 运行写好的工程:
- 6.scrapy crawl first_one
-
- 这时我们运行就会出现大量日志,或者出错所以
- 在settings文件中:
- 将ROBOTSTXT_OBEY = True
- COOKIES_ENABLED = True改为
- ROBOTSTXT_OBEY = False(不遵守robots协议)(必要)
- COOKIES_ENABLED = True(cookie设定)
- 添加
- LOG_LEVEL='ERROR'(只自动输出错误信息)(必要)
- 保存后结合scrapy crawl first_one运行即可
-
创建的文件和settings文件是这样的:
first_one文件是这样的:
- import scrapy
-
- class FirstOneSpider(scrapy.Spider):
- # 文件名称:爬虫源文件唯一标识
- name = 'first_one'
- # 允许的域名,允许start_urls中那些url可以被自动发送请求
- # 若无则start_urls中所有都会自动请求,一般不用allowed_domains
- allowed_domains = ['www.baidu.com']
- # 起始url列表
- start_urls = ['http://www.baidu.com/']
-
- # 用于数据解析,response表示start_urls中url请求后返回的数据
- # 对每一个url都会调用一次parse函数
- def parse(self, response):
- pass
-
在scrapy中的框架包含的方法基本都是我们之前所讲的,有一些不同直接在实例代码中讲解
- import scrapy
-
- # 关于xpath表达式这里就不在过多说明
- class FirstOneSpider(scrapy.Spider):
- name = 'first_one'
- # allowed_domains = ['duanzixing.com']
- # 起始url列表
- start_urls = ['https://duanzixing.com/']
-
- def parse(self, response):
- # scrapy中不用response.text
- # 这里的xpath是scrapy中的,不是etree中的
- # xpath结果是一个集合对象,所要的数据在data标签后
- # 后加上 .extract() 就相当于将取出data标签后的结果,就是我们想要的结果
- # 若对于xpath结果对象的列表使用 .extract() 就是每个对象data内数据的列表
- # 当然有一个方法 .extract_first()就是对于xpath结果对象的列表取出data内容后取出第一个元素内容
- # 列表.extract_first() 相当于 列表.extract()[0]
- # 由于xpath通常返回列表,所以我们经常使用.extract_first()
- article_list=response.xpath('/html/body/section/div/div/article')
- print(len(article_list))
- for article in article_list:
- title = article.xpath('./header/h2/a/text()').extract_first()
- content=article.xpath('./p[@class="note"]/text()').extract_first()
- print(title+':')
- print(content+'\n\n')
-
结果:
直接看代码
- import scrapy
-
- class FirstOneSpider(scrapy.Spider):
- name = 'first_one'
- start_urls = ['https://duanzixing.com/']
-
- def parse(self, response):
- article_list=response.xpath('/html/body/section/div/div/article')
- print(len(article_list))
- all_data=[]
- for i,article in enumerate(article_list):
- title = article.xpath('./header/h2/a/text()').extract_first()
- content=article.xpath('./p[@class="note"]/text()').extract_first()
- dic={
- "index":i+1 # 定义字典index方便我们查看段子顺序和存储方式
- ,"title":title
- ,"content":content
- }
- all_data.append(dic)
-
- # 返回值就是我们想要存储在文件中的数据内容
- # 返回值需要是字典或者由字典组成的列表,具体原因不知道但是我们可以通过存储方式进行推测
- return all_data
-
- # 运行时比较特殊,终端输入 :
- scrapy crawl first_one -o duanzi.csv
- # 就回把数据放在duanzi.csv文件中
- # 并且文件只支持'json', 'jsonlines', 'jl', 'csv', 'xml', 'marshal', 'pickle'
-
将数据文件通过excell打开就是这样的,说明在存储的时候会吧相同标签的放在一起,这可能也是为什么需要是字典格式
基本流程为:
- import scrapy
-
- class FirstscrapyItem(scrapy.Item):
- # define the fields for your item here like:
- # name = scrapy.Field()
- # 定义属性个数就取决于数据类别,我们有index,title,content三种
- index=scrapy.Field()
- title=scrapy.Field()
- content=scrapy.Field()
- # 对于Field类型可以理解为万能数据类型,所有的类别都只能用Field定义
- # 由于这三者是类里面的属性,所以我们需要通过创建FirstscrapyItem类对象进行调用
- # 但是不能通过.进行调用,这个class比较特别需要通过 对象["属性名"]来调用属性
-
像这样
- import scrapy
- # 导入类(会报红,没关系)
- from firstscrapy.items import FirstscrapyItem
-
- class FirstOneSpider(scrapy.Spider):
- name = 'first_one'
- start_urls = ['https://duanzixing.com/']
-
- def parse(self, response):
- article_list=response.xpath('/html/body/section/div/div/article')
- print(len(article_list))
- for i,article in enumerate(article_list):
- title = article.xpath('./header/h2/a/text()').extract_first()
- content=article.xpath('./p[@class="note"]/text()').extract_first()
-
- # 实例化一个FirstscrapyItem类型的Item对象,用来接受存储数据
- item=FirstscrapyItem()
- item['index']=i+1
- item['title']=title
- item['content']=content
-
- # 将item对象提交给管道
- yield item
-
于是就成了这样
- class FirstscrapyPipeline(object): #(object)可写可不写,默认有
-
- fp=None # 定义一个属性用于存储文件打开名称
-
- # 重写open_spider方法,open_spider有着爬虫开始时,执行一次的特点
- # 据此特性用来打开文件
- def open_spider(self,spider):
- print("爬虫开始时,我只执行一次")
- self.fp=open("duanzi.txt",mode="w",encoding="utf-8")
-
- # 重写close_spider方法,close_spider有着爬虫结束时,执行一次的特点
- # 据此特性用来关闭文件
- def close_spider(self,spider):
- print("爬虫结束时,我只执行一次")
- self.fp.close()
-
-
- # 该方法是用来接收第四步传过来的item对象的
- # item就是我们第四步传过来的Item对象值
- # 且一次只能接收一次Item对象,所以该方法会被调用多次
- def process_item(self, item, spider):
- # print(item) # 一会打印看效果,可以直观的看出item形式
- # 存储数据
- self.fp.write(str(item["index"])+'\t'+item["title"]+":\n"+item["content"]+'\n\n')
- return item
-
-
终端运行:scrapy crawl first_one
运行结果:
终端输出:
(duanzi.txt文件)
在我们爬取段子网中多个页面的时候,我们该怎么做呢?
看一下网址:
- 第一页:https://duanzixing.com/page/1/
- 第二页:https://duanzixing.com/page/2/
- 第三页:https://duanzixing.com/page/3/
-
现在你可能豁然开朗,但是怎么才能让start_urls中自动包含多个网址呢?
显然是不行的,我们只能手动进行复制粘贴,那你就比较烦,所以我们就有了在parse中添加语句以回调函数的形式调用parse,继续运行下一个网址对应的数据
下面我们直接在代码中进行讲解
主文件代码(first_one.py):
- import scrapy
- # 导入类
- from firstscrapy.items import FirstscrapyItem
- class FirstOneSpider(scrapy.Spider):
- name = 'first_one'
- start_urls = ['https://duanzixing.com/']
-
- # 这里我们需要添加属性,因为我们要想在FirstOneSpider这个类中参数一直存在,通过属性让多次运行方法之间简历联系
- duanzi_num=1 # 定义属性,记录段子总个数
- page_num=2 # 定义属性,记录段子页码,也用于合成网址
- urls="https://duanzixing.com/page/{0}/" # 合成网址的“原料”
-
- def parse(self, response):
- article_list=response.xpath('/html/body/section/div/div/article')
-
- # 输出每一页有多少段子
- print("第{0}页有:".format(self.page_num-1),len(article_list),"个")
- for article in article_list:
- title = article.xpath('./header/h2/a/text()').extract_first()
- content=article.xpath('./p[@class="note"]/text()').extract_first()
- # 实例化一个FirstscrapyItem类型的Item对象,用来接受存储数据
- item=FirstscrapyItem()
- item['index']=self.duanzi_num # 段子index
- self.duanzi_num+=1
- item['title']=title
- item['content']=content
- # 将item对象提交给管道
- yield item
-
- # 爬取前四页
- if self.page_num<5:
- # 合成网址
- new_url=self.urls.format(self.page_num)
- self.page_num+=1
-
- # 这句是最重要的,yield是关键字,scrapy.Request为scrapy中get形式的方法
- # 对应的post是:yield scrapy.FromRequest(url,callback,formdata)
- # 当传入url后就会把从url中获得的响应数据传给callback对应的回调函数中,然后运行回调函数
- yield scrapy.Request(url=new_url,callback=self.parse)
-
终端:
duanzi.txt文件:
对于不同界面的数据,我们需要多次请求,那么我们如何通过scrapy实现呢?
这里有两个问题:
1.不同界面数据需要不同的解析方法,parse一个方法显然不能满足我们(重建另一个和parse功能相同的方法content_parse)
2.在用管道存储时,我们在主文件中用到的Item对象在运行parse时进入我们重建的方法时,不能作用于重建的方法,那么我们如何实现对同一个Item进行整合呢?(使用scrapy.Request中meta参数)
爬取网址的电影名和电影对应的简介
- import scrapy
- from firstscrapy.items import FirstscrapyItem
- class FirstOneSpider(scrapy.Spider):
- name = 'first_one'
- start_urls = ['https://www.k8jds.com/index.php/vod/show/id/5/lang/%E5%9B%BD%E8%AF%AD.html']
-
- movie_index=1 # 定义属性,记录段子总个数
-
- def parse(self, response):
- subjects=response.xpath('/html/body/div[2]/div/div[2]/div/div[2]/ul/li')
- for subject in subjects:
- title=subject.xpath('./div/a/@title').extract_first()
- content_url="https://www.k8jds.com"+subject.xpath('./div/a/@href').extract_first()
-
- item=FirstscrapyItem()
- item["index"]=self.movie_index
- self.movie_index+=1
- item["title"]=title
-
- yield scrapy.Request(url=content_url
- ,callback=self.content_parse# 定义回调函数(方法)
- ,meta={"item":item}# 请求传参,和回调函数参数建立联系
- )
-
- def content_parse(self,response):
- item=response.meta['item']# 取出参数
- content=response.xpath('/html/body/div[2]/div/div[1]/div[4]/div/div[2]/div/span[1]/text()').extract_first()
- # 避免有None类型数据
- if content==None:
- item['content']="None"
- else:
- item['content']=content
- yield item
-
- class FirstscrapyPipeline(object):
-
- fp=None
-
- def open_spider(self,spider):
- print("爬虫开始时,我只执行一次")
- self.fp=open("movies.txt",mode="w",encoding="utf-8")
-
-
- def close_spider(self,spider):
- print("爬虫结束时,我只执行一次")
- self.fp.close()
-
-
- def process_item(self, item, spider):
- self.fp.write(str(item["index"])+"\n"+item["title"]+":\n"+item["content"]+'\n\n')
- return item
-
-
- import scrapy
-
- class FirstscrapyItem(scrapy.Item):
- # 定义属性个数就取决于数据类别,我们有index,title,content三种
- index=scrapy.Field()
- title=scrapy.Field()
- content=scrapy.Field()
-
- BOT_NAME = 'firstscrapy'
-
- SPIDER_MODULES = ['firstscrapy.spiders']
- NEWSPIDER_MODULE = 'firstscrapy.spiders'
-
-
- # Crawl responsibly by identifying yourself (and your website) on the user-agent
- USER_AGENT = 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Mobile Safari/537.36 Edg/91.0.864.59'
-
- # Obey robots.txt rules
- ROBOTSTXT_OBEY = False
- # 不遵循robots协议
-
- LOG_LEVEL='ERROR'
- # 运行结果时除了代码指令外只输出出错信息
-
- ITEM_PIPELINES = {
- # 300表示管道优先级
- 'firstscrapy.pipelines.FirstscrapyPipeline': 300,
- }
-
我们发现是无序的,经过分析跟我们新定义的content_parse方法有关,先把所有的parse运行完再取运行content_parse,导致无序,这时因为在scrapy框架中自动开启了多线程运行,在settings.py文件中有一项CONCURRENT_REQUESTS,就是指线程个数,默认16,改为1,就可以了
- 中间件有两种:下载中间件和爬虫中间件
- 下载中间件作用:批量拦截请求和响应
- 拦截请求:
- - 篡改请求的url(一般不用)
- - UA伪装
- - cookie破解
- - 设置代理
- (scrapy中代理必须经过中间件完成, UA伪装和cookie可以在settings.py文件中设置)
- 拦截响应:
- - 篡改响应数据(一般不用)
-
- 中间件在使用时主要是process_exception(可不是process_request啊)方法中的代理操作
-
在middlewares.py文件中:
- from scrapy import signals
-
- import random
-
- class MiddleproDownloaderMiddleware(object):
- # 提供一些UA
- 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"
- ]
-
- # 提供代理地址
- PROXY_http = [
- '153.180.102.104:80',
- '195.208.131.189:56055',
- ]
- PROXY_https = [
- '120.83.49.90:9000',
- '95.189.112.214:35508',
- ]
-
- #拦截所有请求包括:正常和异常
- # 在主文件中所有请求包括start_urls,scrapy.Request都进行拦截
- # 一次请求拦截一次,运行一次process_request方法
- # 参数request是拦截的所有请求,spider是拦截的爬虫实例化对象
- def process_request(self, request, spider):
- # 对每个拦截的请求进行UA伪装(对应着settings.py中的USER_AGENT)
- request.headers['User-Agent'] = random.choice(self.user_agent_list)
-
- # 设置cookie(对应着settings.py中的COOKIES_ENABLED)
- request.headers['Cookie']="********"
-
- # 对每个拦截的请求进行代理操作
- request.meta['proxy'] = 'http://183.146.213.198:80'
- return None
-
- #拦截所有请求返回的响应数据,可对响应数据进行修改
- def process_response(self, request, response, spider):
- #response.text=content意味着将拦截的响应数据进行修改成了content的内容
- return response
-
-
- # 拦截发生异常的请求,拦截到异常请求就会运行一次
- # 作用是将异常的请求经过某种方式(通常是代理)的修改,使之成为正常请求,并返回该请求重新请求
- # 参数request是拦截的异常请求,spider是拦截的爬虫实例化对象
- def process_exception(self, request, exception, spider):
- # 分辨url类型,对于异常请求进行代理操作
- 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
-
我们先思考一个问题。下载图片数据时,使用scrapy.Request()请求视频或图片数据,回调函数为空,不能够读取二进制数据。那么就给我们读取二进制数据,带来了非常多的麻烦,所以我们要使用库中封装好的管道方式(即在pipelines.py中)来读取存储二进制数据
- import scrapy
- from firstscrapy.items import FirstscrapyItem
- class FirstOneSpider(scrapy.Spider):
- name = 'first_one'
- start_urls = ['https://pic.netbian.com/4kmeinv/',]
-
- def parse(self, response):
- # 图片名字列表(相信大家都会了)
- picture_name_list = response.xpath("//ul[@class='clearfix']/li/a/img/@alt").extract()
- # url列表
- picture_loc = response.xpath("//ul[@class='clearfix']/li/a/img/@src").extract()
-
- for i in range(len(picture_loc)):
- item=FirstscrapyItem()
- item["title"]=picture_name_list[i]
- item["url"]="https://pic.netbian.com/"+picture_loc[i]
- # 将处理好的item数据传给管道
- yield item
-
- # 导入scrapy中特定的类,为我们提供了数据下载功能
- from scrapy.pipelines.images import ImagesPipeline
- import scrapy
-
- # 之前默认的管道无法帮助我们请求二进制数据,因此我们要重新写一个管道,命名可以和之前的一样,也可以不一样。
- # 我们通过这个管道对于ImagesPipeline的继承,对于里面的方法进行重写,来实现图片数据读取储存
- # 在这里我们需要重写三个方法,并且这三个方法是连续运行的
- class FirstscrapyPipeline(ImagesPipeline):
- # 根据图片地址发起请求。
- def get_media_requests(self, item, info):
- yield scrapy.Request(url=item["url"],meta={"item":item})# 不需要callback
-
- # get_media_requests结束后立即运行自动file_path
- # 通过上一部meta={"item":item}传参取出title作为数据名称
- # 该方法返回值作为图片存储名(仅仅对于我们来说只有这一个作用)
- def file_path(self, request, response=None, info=None, *, item=None):
- item=request.meta["item"]
- filePath=item["title"]+'.jpg'
- return filePath
-
- # 将item传递给下一个即将被执行的管道类
- def item_completed(self, results, item, info):
- return item
-
- import scrapy
-
- class FirstscrapyItem(scrapy.Item):
- #存储名
- title=scrapy.Field()
- # 存储url
- url=scrapy.Field()
-
- BOT_NAME = 'firstscrapy'
-
- SPIDER_MODULES = ['firstscrapy.spiders']
- NEWSPIDER_MODULE = 'firstscrapy.spiders'
- # UA
- USER_AGENT = 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Mobile Safari/537.36 Edg/91.0.864.59'
- # 不遵循robots协议
- ROBOTSTXT_OBEY = False
- # 运行结果时除了代码指令外只输出出错信息
- LOG_LEVEL='ERROR'
-
- # 创建名为Picture_home的文件夹,pipelines.py文件中FirstscrapyPipeline.file_path方法返回值会自动放在该目录下
- IMAGES_STORE="./Picture_home"
-
- # 开启32个线程
- CONCURRENT_REQUESTS = 32
-
- ITEM_PIPELINES = {
- # FirstscrapyPipeline需要与管道文件pipelines.py中创建的类名相同
- 'firstscrapy.pipelines.FirstscrapyPipeline': 300,
- }
-
CrawlSpider是通过界面按钮对应的网址进行提取
如何创建一个CrawlSpider呢?
- 在工作空间(其他文件(settings.py......)均已配置好)下终端输入:
- scrapy genspider -t crawl CrawlSpiderName www.xxxx.com
- CrawlSpiderName表示自定义的名字(我这里定为first_crawlSpider)
- www.xxxx.com目标网址,后续可修改
-
先了解一下目标:就是解析出这些网址
- import scrapy
- from scrapy.linkextractors import LinkExtractor
- from scrapy.spiders import CrawlSpider, Rule
-
- class FirstCrawlspiderSpider(CrawlSpider):
- name = 'first_crawlSpider'
- #allowed_domains = ['www.baidu.com']
- start_urls = ['https://pic.netbian.com/4kmeinv/']
-
- # 想要爬取页面中页码按键对应的网址,那么我们要用正则表达式取出都有网址
- # 正则表达式需要包含所有目标按键对应的网址,就是allow填的网址的正则表达式
- # 通过LinkExtractor实例化对象生成link作为Rule第一个参数
- # link叫做链接提取器
- link=LinkExtractor(allow="meinv/index_\d+.ht")
- rules = (
- # 叫做规则解析器
- Rule(link # 实例化好的解析网址的正则表达式
- , callback='parse_item' # 将响应数据通过回调函数传给指定方法
- , follow=False #回续说明含义
- ), # 这个逗号千万不能丢
- )
-
- """
- 运行流程:
- 1. 根据start_urls里面网址返回响应数据
- 2. 通过link中参数allow携带的正则表达式进行响应数据的匹配
- 3. 对于每一个对应的匹配结果会自动根据界面补成真正的网址
- 比如:
- 真正网址:https://pic.netbian.com/4kmeinv/index_3.html>
- 正则匹配结果:kmeinv/index_3.ht
- 那么通过第三步得到的结果依然为https://pic.netbian.com/4kmeinv/index_3.html>
- 也就是说:只要匹配结果是真正网址的子集,那么就会生成返回真正网址,不需要我们手动填补,就是那么神奇
- 4. 将真正的网址一次传给回调函数parse_item中response参数
- """
- def parse_item(self, response):
- print(response)
-
- """
- 结果:
- <200 https://pic.netbian.com/4kmeinv/index_144.html>
- <200 https://pic.netbian.com/4kmeinv/index_2.html>
- <200 https://pic.netbian.com/4kmeinv/index_7.html>
- <200 https://pic.netbian.com/4kmeinv/index_5.html>
- <200 https://pic.netbian.com/4kmeinv/index_4.html>
- <200 https://pic.netbian.com/4kmeinv/index_3.html>
- <200 https://pic.netbian.com/4kmeinv/index_6.html>
-
- 如果没有步奏3自动生成真正网址,结果是:
- <200 meinv/index_144.ht>
- <200 meinv/index_2.ht>
- <200 meinv/index_7.ht>
- <200 meinv/index_5.ht>
- <200 meinv/index_4.ht>
- <200 meinv/index_3.ht>
- <200 meinv/index_6.ht>
- """
-
对于刚才网址解析之解析出来了显示在可视化界面中的七个按钮对应的网址,那么如果我们想要一次全部获取到界面中144个网址该怎么办呢?
对!就是follow设置为True,意思就是针对获取到的所有按钮对应的网址都会作为start_urls里的参数重新运行link和rules,并将重复的销毁,然后传给回调函数parse_item。
另外:follow默认True
将上述代码follow改为True结果:
取出了所有
和除了first_crawlSpider.py文件,其他文件图片视频数据的下载完全相同,不再展示,仅仅附上first_crawlSpider.py源码和运行结果
- from scrapy.linkextractors import LinkExtractor
- from scrapy.spiders import CrawlSpider, Rule
- from firstscrapy.items import FirstscrapyItem
-
- class FirstCrawlspiderSpider(CrawlSpider):
- name = 'first_crawlSpider'
- start_urls = ['https://pic.netbian.com/4kmeinv/']
-
- link=LinkExtractor(allow="meinv/index_\d+.ht")
- rules = (
- # 由于共144页,所以设置了follow=False减少实验量
- # follow=True会爬取所有图片,若过多不要忘了在settings.py文件中多设置一些线程啊
- Rule(link, callback='parse_item', follow=False),
- )
-
- def parse_item(self, response):
- # 名称列表
- title_list=response.xpath('//*[@id="main"]/div[3]/ul/li/a/img/@alt').extract()
- # url列表
- url_list=response.xpath('//*[@id="main"]/div[3]/ul/li/a/img/@src').extract()
-
- for i in range(len(title_list)):
- item=FirstscrapyItem()
- item["title"]=title_list[i]
- item["url"]="https://pic.netbian.com"+url_list[i]
-
- yield item
-
结果: