Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架,很是出名,很是强悍。所谓的框架就是一个已经被集成了各类功能(高性能异步下载,队列,分布式,解析,持久化等)的具备很强通用性的项目模板。php
环境的安装:html
mac/linux:pip install scrapy window: - pip install wheel - 下载twisted http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted - 进入下载目录,执行 pip install Twisted‑17.1.0‑cp37‑cp37m‑win_amd64.whl - pip install pywin32 - pip install scrapy
新建一个工程:python
scrapy startproject ProName
ROBOTSTXT_OBEY = False
USER_AGENT = 'xxx'
LOG_LEVEL = 'ERROR'
cd ProName
:进入到工程目录中scrapy genspider spiderName www.xxx.com
scrapy crawl spiderName
# -*- coding: utf-8 -*- import scrapy # 爬虫类: 父类 => Spider class FirstSpider(scrapy.Spider): # 爬虫文件的名称: 当前爬虫源文件的惟一标识 name = 'first' # 容许的域名, 限定爬取的域名 # allowed_domains = ['www.baidu.com'] # 起始的url列表, 列表中存放的url均可以被scrapy进行异步的网络请求 start_urls = ['https://www.baidu.com/', 'https://www.taobao.com/'] # 用做与数据解析 # 参数: response就是响应对象 def parse(self, response): print(response)
scrapy的数据解析与持久化存储:mysql
需求: 爬取 https://dig.chouti.com/ 的文章标题与做者linux
extract()与extract_first(): 从xpath查询结果对象(Selector)中取得数据web
scrapy的持久化存储正则表达式
基于终端指令进行持久化存储redis
只能够将parse方法的返回值存储到本地的磁盘文件(指定形式后缀)中sql
支持: json
, jsonlines
, jl
, csv
, xml
, marshall
, pickle
chrome
scrapy crawl spiderName -o filePath
# 用做与数据解析 # 参数: response就是响应对象 def parse(self, response): div_list = response.xpath('/html/body/main/div/div/div[1]/div/div[2]/div[1]/div') all_data = [] for div in div_list: # 解析标题和做者信息 # xpath在取标签中存储的文本数据时必需要使用extract(),extract_first()进行字符串的单独提取 title = div.xpath('.//div[@class="link-detail"]/a/text()')[0].extract() author = div.xpath('.//span[@class="left author-name"]/text()').extract_first() all_data.append({'title': title, 'author': author}) # 基于终端指令的持久化存储 # scrapy crawl spiderName -o filePath return all_data
基于管道进行持久化存储(经常使用)
编码流程:
# settings.py # 开启管道,注册管道 ITEM_PIPELINES = { # 300表示优先级,数值越小优先级越高 'firstblood.pipelines.FirstbloodPipeline': 300, }
# items.py import scrapy class FirstbloodItem(scrapy.Item): # define the fields for your item here like: # name = scrapy.Field() # Field类型视为一个万能的数据类型 title = scrapy.Field() author = scrapy.Field()
# pipelines.py import pymysql class FirstbloodPipeline(object): conn = None cursor = None # 重写父类方法,该方法只会在爬虫开始时被执行一次 def open_spider(self, spider): self.conn = pymysql.Connection(host='127.0.0.1', port=3306, user='root', password='123', db='scrapy', charset='utf8') # process_item方法调用后就能够接收爬虫类提交来的item对象 def process_item(self, item, spider): title = item['title'] author = item['author'] sql = 'insert into chouti values (%s, %s)' self.cursor = self.conn.cursor() # 事务处理 try: self.cursor.execute(sql, [author, title]) self.conn.commit() except Exception as e: print(e) # 回滚事务 self.conn.rollback() return item # 将item传递给下个管道类 def close_spider(self, spider): self.cursor.close() self.conn.close()
# first.py # -*- coding: utf-8 -*- import scrapy from firstblood.items import FirstbloodItem class FirstSpider(scrapy.Spider): name = 'first' start_urls = ['https://dig.chouti.com/'] # 用做与数据解析 # 参数: response就是响应对象 def parse(self, response): div_list = response.xpath('/html/body/main/div/div/div[1]/div/div[2]/div[1]/div') for div in div_list: title = div.xpath('.//div[@class="link-detail"]/a/text()')[0].extract() author = div.xpath('.//span[@class="left author-name"]/text()').extract_first() # 实例化一个item类型的对象 item = FirstbloodItem() # 给item对象的属性赋值 item['title'] = title item['author'] = author yield item # 将item提交给管道
案例:
爬取http://www.521609.com/daxuexiaohua/的图片
scrapy中封装了一个管道类(ImagesPipeline
),基于该管道类,能够实现图片资源的请求和永久化存储
编码流程:
1.在爬虫文件中解析出图片的地址
2.将图片地址封装到item中且提交给管道
import scrapy from getImg.items import GetimgItem class ImgSpider(scrapy.Spider): name = 'Img' # allowed_domains = ['www.xxx.com'] start_urls = ['http://www.521609.com/daxuemeinv/'] # 定义一个通用的url模板 url = 'http://www.521609.com/daxuemeinv/list8%d.html' pageNum = 1 def parse(self, response): li_list = response.xpath('//*[@id="content"]/div[2]/div[2]/ul/li') for li in li_list: img_src = 'http://www.521609.com' + li.xpath('./a[1]/img/@src').extract_first() item = GetimgItem() item['img_src'] = img_src yield item if self.pageNum < 5: self.pageNum += 1 new_url = self.url % self.pageNum # 手动发请求,实现深度爬取 yield scrapy.Request(new_url, callback=self.parse)
3.管道文件中自定义一个管道类(继承ImagesPipeline
)
4.重写三个方法:
def get_media_requests(self, item, info):
def file_path(self, request, response=None, info=None):
def item_completed(self, results, item, info):
import scrapy from scrapy.pipelines.images import ImagesPipeline class GetimgPipeline(ImagesPipeline): # 该方法是用做与请求发送的 def get_media_requests(self, item, info): # 对item中的img_src进行请求发送 # 手动进行请求发送 yield scrapy.Request(url=item['img_src']) # 用于指定文件路径(文件夹 + 文件名称) def file_path(self, request, response=None, info=None): # 存到settings中指定的文件夹中 return request.url.split('/')[-1] # 将当前的item提交给下个管道类 def item_completed(self, results, item, info): return item
5.在配置文件中开启管道且加上IMAGES_STORE配置
# 自定义文件夹路径 IMAGES_STORE = './imgLibs'
在scrapy中如何进行手动请求发送?
yield scrapy.Request(url, callback)
在scrapy中如何进行post请求的发送?
# formdata是post提交的数据 yield scrapy.FormRequest(url, callback, formdata={})
如何对起始的url进行post请求的发送?
# 重写父类的start_requests(self)方法 def start_requests(self): for url in self.start_urls: yield scrapy.FormRequest(url, callback=self.parse, formdata={})
增长并发: 默认scrapy开启的并发线程为32个,能够适当进行增长。在settings配置文件中修改CONCURRENT_REQUESTS = 100值为100,并发设置成了为100。 下降日志级别: 在运行scrapy时,会有大量日志信息的输出,为了减小CPU的使用率。能够设置log输出信息为INFO或者ERROR便可。在配置文件中编写:LOG_LEVEL = ‘ERROR’ 禁止cookie: 若是不是真的须要cookie,则在scrapy爬取数据时能够禁止cookie从而减小CPU的使用率,提高爬取效率。在配置文件中编写:COOKIES_ENABLED = False 禁止重试: 对失败的HTTP进行从新请求(重试)会减慢爬取速度,所以能够禁止重试。在配置文件中编写:RETRY_ENABLED = False 减小下载超时: 若是对一个很是慢的连接进行爬取,减小下载超时能够能让卡住的连接快速被放弃,从而提高效率。在配置文件中进行编写:DOWNLOAD_TIMEOUT = 1 超时时间为10s
什么叫深度爬取?
如何实现请求传参?
Request(url,callback,meta={}):能够将meta字典传递给callback
callback接收item:response.meta
需求:
爬取https://www.4567tv.tv/index.php/vod/show/class/%E5%8A%A8%E4%BD%9C/id/1.html的每一页的电影名称和简介.
import scrapy from moviePro.items import MovieproItem class MovieSpider(scrapy.Spider): name = 'movie' # allowed_domains = ['www.xxx.com'] start_urls = ['https://www.4567tv.tv/index.php/vod/show/class/%E5%8A%A8%E4%BD%9C/id/1.html'] # 通用url模板 url = 'https://www.4567tv.tv/index.php/vod/show/class/动做/id/1/page/%d.html' pageNum = 2 # 解析电影名称和详情页的url def parse(self, response): li_list = response.xpath('/html/body/div[1]/div/div/div/div[2]/ul/li') for li in li_list: title = li.xpath('./div/div/h4/a/text()').extract_first() detail_url = 'https://www.4567tv.tv' + li.xpath('./div/div/h4/a/@href').extract_first() item = MovieproItem() item['title'] = title # 手动请求发送, 将meta字典传递给指定的callback(请求传参) yield scrapy.Request(detail_url, callback=self.parse_detailxxx, meta={'item': item}) if self.pageNum < 5: new_url = format(self.url % self.pageNum) self.pageNum += 1 yield scrapy.Request(new_url, callback=self.parse) # 解析详情页中的电影简介 def parse_detailxxx(self, response): item = response.meta['item'] desc = response.xpath('/html/body/div[1]/div/div/div/div[2]/p[5]/span[2]/text()').extract_first() item['desc'] = desc yield item
Scrapy Engine(引擎): 负责Spider、ItemPipeline、Downloader、Scheduler中间的通信,信号、数据传递等。
Scheduler(调度器): 它负责接受引擎发送过来的Request请求,并按照必定的方式进行整理排列,入队,当引擎须要时,交还给引擎。
Downloader(下载器):负责下载Scrapy Engine(引擎)发送的全部Requests请求,并将其获取到的Responses交还给Scrapy Engine(引擎),由引擎交给Spider来处理,
Spider(爬虫):它负责处理全部Responses,从中分析提取数据,获取Item字段须要的数据,并将须要跟进的URL提交给引擎,再次进入Scheduler(调度器).
Item Pipeline(管道):它负责处理Spider中获取到的Item,并进行进行后期处理(详细分析、过滤、存储等)的地方。
Downloader Middlewares(下载中间件):你能够看成是一个能够自定义扩展下载功能的组件。 (经常使用,能够取到request和response)
Spider Middlewares(Spider中间件):你能够理解为是一个能够自定扩展和操做引擎和Spider中间通讯的功能组件(好比进入Spider的Responses;和从Spider出去的Requests)
执行流程:
1 引擎:Hi!Spider, 你要处理哪个网站? 2 Spider:老大要我处理xxxx.com。 3 引擎:你把第一个须要处理的URL给我吧。 4 Spider:给你,第一个URL是xxxxxxx.com。 5 引擎:Hi!调度器,我这有request请求你帮我排序入队一下。 6 调度器:好的,正在处理你等一下。 7 引擎:Hi!调度器,把你处理好的request请求给我。 8 调度器:给你,这是我处理好的request 9 引擎:Hi!下载器,你按照老大的下载中间件的设置帮我下载一下这个request请求 10 下载器:好的!给你,这是下载好的东西。(若是失败:sorry,这个request下载失败了。而后引擎告诉调度器,这个request下载失败了,你记录一下,咱们待会儿再下载) 11 引擎:Hi!Spider,这是下载好的东西,而且已经按照老大的下载中间件处理过了,你本身处理一下(注意!这儿responses默认是交给def parse()这个函数处理的) 12 Spider:(处理完毕数据以后对于须要跟进的URL),Hi!引擎,我这里有两个结果,这个是我须要跟进的URL,还有这个是我获取到的Item数据。 13 引擎:Hi !管道 我这儿有个item你帮我处理一下!调度器!这是须要跟进URL你帮我处理下。而后从第四步开始循环,直到获取完老大须要所有信息。 14 管道调度器:好的,如今就作! 注意!只有当调度器中不存在任何request了,整个程序才会中止,(也就是说,对于下载失败的URL,Scrapy也会从新下载。)
scrapy有哪些中间件?
下载中间件的做用:
为何拦截请求?
request.headers['User-Agent'] = 'xxxxx'
request.meta['proxy'] = 'http://ip:port'
为何拦截响应?
案例分析:
爬取网易新闻(国内,国际,军事,航空,无人机)新闻数据的标题和内容,而且调用百度AI对文章内容进行标签提取和文章分类
分析:
1.每个板块下对应的新闻数据都是动态加载出来的
2.会对五个板块的响应数据进行数据解析,可是板块对应的响应对象是不包含动态加载的新闻数据,目前获取的每个板块对应的响应对象是不知足需求的响应对象
3.将不知足需求的5个响应对象(工程中一共会有1+5+n),修改为知足需求的响应对象。
# 爬虫文件中 import scrapy from NewsPro.items import NewsproItem from selenium import webdriver import time from aip import AipNlp class NewsSpider(scrapy.Spider): name = 'news' # allowed_domains = ['https://news.163.com'] start_urls = ['https://news.163.com/'] # 5个板块的url model_urls = [] # 打开selenium browser = webdriver.Chrome(executable_path='./chromedriver.exe') time.sleep(1) # 解析出每一个板块对应的url def parse(self, response): li_index = [3, 4, 6, 7, 8] li_list = response.xpath('//div[@class="ns_area list"]/ul/li') for index in li_index: # 解析到每一个板块的url model_url = li_list[index].xpath('./a/@href').extract_first() self.model_urls.append(model_url) # 对板块的url进行请求发送获取每个板块对应的页面源码数据 yield scrapy.Request(model_url, self.parse_model) # 请求每一个板块的页面源码数据,解析出新闻的标题和详情页url def parse_model(self, response): div_list = response.xpath('//div[@class="ndi_main"]/div') for div in div_list: item = NewsproItem() new_url = div.xpath('./div/div[1]/h3/a/@href').extract_first() title = div.xpath('./div/div[1]/h3/a/text()').extract_first() if new_url and title: item['title'] = title # 对新闻的详情页进行请求发送,获取详情页的页面源码数据 yield scrapy.Request(new_url, self.parse_new, meta={'item': item}) # 解析新闻详情页数据,并提交给管道 def parse_new(self, response): content = response.xpath('//*[@id="endText"]//text()').extract() content = ''.join(content) item = response.meta['item'] item['content'] = content yield item # 关闭selenium浏览器 def closed(self, spider): self.browser.quit()
from scrapy import signals import random from time import sleep from scrapy.http import HtmlResponse user_agent_list = ['xxx', 'xxx'] class NewsproDownloaderMiddleware(object): def process_request(self, request, spider): # 经过中间件进行UA假装,尽可能每次使用不一样的UA request.headers['User-Agent'] = random.choice(user_agent_list) return None def process_response(self, request, response, spider): # selenium对象 browser = spider.browser url = request.url # 判断是否5个模块的url if url in spider.model_urls: browser.get(url) sleep(1) # 下来滚轴,获取所有的动态加载数据 browser.execute_script('window.scrollTo(0,document.body.scrollHeight)') sleep(1) browser.execute_script('window.scrollTo(0,document.body.scrollHeight)') sleep(1) browser.execute_script('window.scrollTo(0,document.body.scrollHeight)') sleep(1) page_text = browser.page_source # 封装一个response对象,替代不符合要求的response对象 response = HtmlResponse(url=request.url, body=page_text, encoding='utf-8', request=request) return response
用途: 便捷的实现全站数据爬取
scrapy startproject proName
cd proName
scrapy genspider -t crawl spiderName www.xxx.com
核心:
连接提取器(LinkExtractor)
规则解析器(Rule)
follow=True
: 将连接提取器继续做用到连接提取到的连接所对应的页面源码中,实现深度爬取案例:
爬取http://wz.sun0769.com/html/top/report.shtml中全部页文章的标题和内容
class LxSpider(CrawlSpider): name = 'lx' # allowed_domains = ['www.xxx.com'] start_urls = ['http://wz.sun0769.com/html/top/report.shtml'] # 连接提取器 link = LinkExtractor(allow=r'page=\d+') link_1 = LinkExtractor(allow=r'page=$') link_detail = LinkExtractor(allow=r'question/\d+/\d+\.shtml') rules = ( # 实例化一个Rule(规则解析器)对象 # follow=True: 将连接提取器 继续 做用到连接提取到的连接所对应的页面源码中 Rule(link_1, callback='parse_item', follow=False), Rule(link, callback='parse_item', follow=True), Rule(link_detail, callback='parse_item_detail', follow=False), ) # 数据解析,是用来解析连接提取器提取到的连接所对应的页面 def parse_item(self, response): tr_list = response.xpath('/html/body/div[8]/table[2]//tr') for tr in tr_list: title = tr.xpath('./td[3]/a/text()').extract_first() num = tr.xpath('./td[1]/text()').extract_first() item = Item2() item['title'] = title item['num'] = num yield item # 解析详情页中的新闻内容 def parse_item_detail(self, response): content = ''.join(response.xpath('/html/body/div[9]/table[2]//tr[1]/td//text()').extract()) num = response.xpath('/html/body/div[9]/table[1]//tr/td[2]/span[2]/text()').extract_first().split(':')[-1] item = Item1() item['content'] = content item['num'] = num yield item
注意, 若是想将不一样页面的数据统一保存,使用Crawl Spider不是很方便,由于它没法进行请求传参.
什么是分布式?
须要搭建一个由n台电脑组成的机群,而后在每一台电脑中执行同一组程序,让其对同一个网络资源进行联合且分布的数据爬取,大幅度提高爬取效率
实现方式:
scrapy
+ scrapy_redis
组件实现的分布式.注意, 原生的scrapy是不能够实现分布式的,为何?
scrapy-redis
组件的做用是什么?
实现流程:
pip install scrapy-redis
scrapy startproject proName
cd proName
修改爬虫文件
1. 导入: from scrapy_redis.spiders import RedisCrawlSpider from scrapy_redis.spiders import RedisSpider 将当前爬虫类的父类修改成导入的类 删除allowed_domains和start_urls 添加一个redis_key = 'xxx'属性,表示调度器队列的名称 根据常规形式编写爬虫文件后续的代码
修改settings.py配置文件
# 指定管道 ITEM_PIPELINES = { 'scrapy_redis.pipelines.RedisPipeline': 400 } # 指定调度器 # 增长了一个去重容器类的配置, 做用使用Redis的set集合来存储请求的指纹数据, 从而实现请求去重的持久化 DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter" # 使用scrapy-redis组件本身的调度器 SCHEDULER = "scrapy_redis.scheduler.Scheduler" # 配置调度器是否要持久化, 也就是当爬虫结束了, 要不要清空Redis中请求队列和去重指纹的set。若是是True, 就表示要持久化存储, 就不清空数据, 不然清空数据 SCHEDULER_PERSIST = True # 指定redis数据库 REDIS_HOST = '127.0.0.1' REDIS_PORT = 6379
修改redis的配置文件redis.windows.conf
关闭默认绑定 56行: # bind 127.0.0.1 关闭保护模式 75行: protected-mode no
启动redis的服务端(携带配置文件)和客户端
启动分布式的程序
cd 到spiders文件夹内 scrapy runspider xxx.py
向调度器的队列中添加一个起始的url
队列是存在于redis中的 redis的客户端中: lpush sun www.xxx.com
案例:
# spiders文件中 from scrapy.linkextractors import LinkExtractor from scrapy.spiders import CrawlSpider, Rule from scrapy_redis.spiders import RedisCrawlSpider from fenbushi.items import FenbushiItem class FbsSpider(RedisCrawlSpider): name = 'fbs' # allowed_domains = ['www.xxx.com'] # start_urls = ['http://www.xxx.com/'] redis_key = 'sun' # 可被共享的调度器队列的名称,即须要添加起始url的redis中的set link = LinkExtractor(allow=r'page=\d+') rules = ( Rule(link, callback='parse_item', follow=True), ) def parse_item(self, response): tr_list = response.xpath('/html/body/div[8]/table[2]//tr') for tr in tr_list: title = tr.xpath('./td[3]/a/text()').extract_first() item = FenbushiItem() item['title'] = title yield item
概念: 监测网站数据更新
核心技术: 去重
适合使用增量式的网站:
记录表是以怎样的形式存在于哪?
推荐使用redis的set充当记录表
案例:
监测https://www.4567tv.tv/index.php/vod/show/id/1/page/1.html的电影标题与简介
import scrapy from scrapy.linkextractors import LinkExtractor from scrapy.spiders import CrawlSpider, Rule from redis import Redis from zlsPro.items import ZlsproItem class ZlsSpider(CrawlSpider): name = 'zls' start_urls = ['https://www.4567tv.tv/index.php/vod/show/id/1/page/1.html'] # 解析每一页的页码连接 link = LinkExtractor(allow=r'1/page/\d+\.html') rules = ( Rule(link, callback='parse_item', follow=False), ) conn = Redis(host='127.0.0.1', port=6379) def parse_item(self, response): li_list = response.xpath('/html/body/div[1]/div/div/div/div[2]/ul/li') for li in li_list: detail_url = 'https://www.4567tv.tv' + li.xpath('./div/a/@href').extract_first() ex = self.conn.sadd('urls', detail_url) if ex == 1: # detail_url不存在于redis的set中 yield scrapy.Request(detail_url, self.parse_detail) else: # detail_url存在于redis的set中 print('无数据更新') def parse_detail(self, response): title = response.xpath('/html/body/div[1]/div/div/div/div[2]/h1/text()').extract_first() desc = response.xpath('/html/body/div[1]/div/div/div/div[2]/p[5]/span[3]/text()').extract_first() print(title, '是新数据') item = ZlsproItem() item['title'] = title item['desc'] = desc yield item
robots: 无论它..
UA检测: UA假装
图片懒加载: 获取相应的伪属性对应的值
验证码: 打码平台,selenium
cookie: session对象自动记录cookie
动态加载的数据: 使用抓包工具,获取动态加载数据对应的url
动态变化的请求参数: 通常隐藏在页面源码中
IP检测: IP代理池
js混淆: js反混淆
js加密: 找到对应的js代码,使用PyExecJS运行js代码