爬虫框架scrapy
1. scrapy
安装
-
框架:就是一个具有很强通用行且已经集成了很多功能的项目模板
-
如何去学习一个框架:
- 学习框架封装好的各种功能
- 了解每一个功能的特性和优劣
-
scrapy
- 高性能的数据解析
- 高性能的持久化存储
- 中间件
- 分布式
- 异步的数据下载(基于twisted实现)
-
pyspider
-
环境安装
- Linux:pip install scrapy
- windows:
- pip install wheel
- 下载twisted http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
- 进入下载目录,执行
pip install Twisted-17.1.0-cp36-cp36m-win_amd64.whl
- pip install pywin32
- pip install scrapy
2. 基本使用
-
创建一个工程:
scrapy stratproject PRONAME
-
cd PRONAME
-
在爬虫文件夹(spiders)中创建一个爬虫文件:
scrapy genspdier spiderName www.xxx.com
-
配置文件:
- 进行UA伪装
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36
- 不遵从robots
- 日志等级的指定 LOG_LEVEL = 'ERROR'
- 进行UA伪装
-
执行工程:
scrapy crawl spiderName
1. 使用小案例,爬取糗事百科文本不存储
# 1.在进行了以上配置操作后,在创建的spiderName中类中操作
class FirstSpider(scrapy.Spider):
name = 'first'
# 允许的域名 一般情况下注释掉
# allowed_domains = ['www.baidu.com']
# 起始url列表 作用:如果列表不为空,列表中存放的url都会被scrapy自动地进行请求发送
start_urls = ['https://www.qiushibaike.com/text/']
# 就是用来将start_urls中url请求到的数据进行数据解析
def parse(self, response):
div_list = response.xpath('//div[@id="content-left"]/div')
for div in div_list:
# 解析出的字符创都存储在了Selector对象中
# author = div.xpath('./div[1]/a[2]/h2/text()')[0]
# content = div.xpath('./a/div/span//text()')
# author = div.xpath('./div[1]/a[2]/h2/text()')[0].extract()
# 将单独的Selector对象中存储的字符串提取出来
author = div.xpath('./div[1]/a[2]/h2/text()').extract_first() # 等价于[0].extract()
# 是由列表调用的,extract会依次作用到每一个列表元素中
content = div.xpath('./a/div/span//text()').extract()
content = ''.join(content) # 将列表元素拼接成字符串
print(author,content)
2. 使用小案例-糗事百科文本持久化存储 两种方式 三种保存方式
- 方式一: 基于终端指令
- 使用:可以将parse方法的返回值存储到本地磁盘文件中
def parse(self, response):
div_list = response.xpath('//div[@id="content-left"]/div')
# 基于终端指令的持久化存储
all_data = []
for div in div_list:
# 解析出的字符创都存储在了Selector对象中
# author = div.xpath('./div[1]/a[2]/h2/text()')[0]
# content = div.xpath('./a/div/span//text()')
# author = div.xpath('./div[1]/a[2]/h2/text()')[0].extract()
# 将单独的Selector对象中存储的字符串提取出来
author = div.xpath('./div[1]/a[2]/h2/text()').extract_first()
# 是由列表调用的,extract会依次作用到每一个列表元素中
content = div.xpath('./a/div/span//text()').extract()
content = ''.join(content)
dic = {
'author': author,
'content': content,
}
all_data.append(dic)
return all_data
### 接下来在终端中执行命令
scrapy scrawl first -o qiushibaike.csv # 咦,为什么是CSV而不是常见的TXT格式呢,好我们试一下TXT
scrapy scrawl first -o qiushibaike.txt
# 你会发现报一个错误
"""
crawl: error: Unrecognized output format 'txt', set one using the '-
t' switch or as a file extension from the supported list ('json', 'j
sonlines', 'jl', 'csv', 'xml', 'marshal', 'pickle')
"""
# 他指出了我们保存文件的后缀名只能是上面列表中的指定的后缀名,那么这么看来,就是基于终端指令的局限性了,所以在实际应用中我们很少去使用。这种持久化的好处就是很便捷,局限性强,只能将数据保存到本地中。
方式二: 基于管道
- 操作流程
- 数据解析
- 在item类中进行相关属性定义
- 将解析的数据封装到item类型的对象中
- 将item对象提交给管道
- 管道接受item然后调用管道类中的process_item方法进行数据的持久化存储
存储方式一:文本文件
爬虫程序
from firstblood.items import FirstbloodItem
# 基于管道持久化数据
def parse(self, response):
div_list = response.xpath('//div[@id="content-left"]/div')
all_data = []
for div in div_list:
author = div.xpath('./div[1]/a[2]/h2/text()').extract_first()
# 是由列表调用的,extract会依次作用到每一个列表元素中
content = div.xpath('./a/div/span//text()').extract()
content = ''.join(content)
# 实例化item对象 再循环内部
item = FirstbloodItem()
# 将解析到的数据封装到item中
item['author'] = author
item['content'] = content # 将解析到的content存储到item对象中content属性中
# 将item提交给管道
yield item # 循环多少次就是提交多少次
item程序
import scrapy
class FirstbloodItem(scrapy.Item):
# define the fields for your item here like:
author = scrapy.Field() # 字符串 那么二进制的数据 流数据呢?
content = scrapy.Field() # Field() 他是一个万能的数据类型,可以存任意类型的数据
管道程序
class FirstbloodPipeline(object):
f = None
# 重写父类的两个方法之一,开启时只调用一次
def open_spider(self, spider):
print('开始爬虫')
self.f = open('qiushi.txt', 'w', encoding='utf8')
def process_item(self, item, spider):
"""
process_item每接受一个item就会被调用一次
:param item: 就是用来接收爬虫文件提交过来的item对象
:param spider:
:return:
"""
print(item)
self.f.write(item['author']+':'+item['content']+'\n')
return item
# 重写父类的两个方法之一
def close_spider(self,spider):
print('结束爬虫')
self.f.close()
管道开启配置
ITEM_PIPELINES = {
'firstblood.pipelines.FirstbloodPipeline': 300,
# 300 表示的优先级 意味着可以放多个管道类 数值越小优先级越高
}
存储方式二:存入MySQL中
创建数据库
show databases;
create database spider;
use database;
create table qiushi (author varchar(50),content varchar(8000))
创建管道类
import pymysql
class mysqlPileLine(object):
conn = None
cursor = None
def open_spider(self, spider):
# 连接数据库
self.conn = pymysql.Connect(host='127.0.0.1', port=3306, user='root', password='root', db='spider',
charset='utf8')
# # 执行SQL语句 接下来创建游标对象 游标是可以多次创建的
# self.cursor = self.conn.cursor()
print(self.conn)
def process_item(self, item, spider):
sql = 'insert into qiubai values ("{}","{}")'.format(item['author'], item['content'])
# 执行SQL语句 接下来创建游标对象 游标是可以多次创建的
self.cursor = self.conn.cursor()
# 进行事务处理
try:
self.cursor.execute(sql)
# 提交数据
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()
注册管道类
ITEM_PIPELINES = {
'firstblood.pipelines.FirstbloodPipeline': 300,
# 300 表示的优先级 意味着可以放多个管道类 数值越小优先级越高
'firstblood.pipelines.mysqlPileLine': 301,
}
存储方式三 存入Redis
管道类
class redisPileLine(object):
conn = None
def open_spider(self, spider):
self.conn = Redis(host='127.0.0.1', port=6379)
def process_item(self, item, spider):
# 将redis的版本切换到2.10.6 pip install -U redis==2.10.6
dic = {
'author': item['author'],
'content': item['content']
}
self.conn.lpush('qiubai', dic)
小细节
- 细节处理:
- 什么时候需要编写多个管道类
- 场景:爬取到的数据一份存入本地文件,一份存储mysql,redis
- process_item中返回值的作用
案例1 校花网爬取
-
基于Spider父类的全站数据爬取
-
手动请求发送
- yield scrapy.Request(url,callback):callback指定解析方法 (get)
- yield scrapy.FormRequest(url,callback,formdata)
- 对起始url列表发起post请求:
def start_requests(self):
for url in self.start_urls:
yield scrapy.FormRequest(url,formdata={},callback=self.parse)
-
注意:scrapy如果发起post请求,scrapy会自动处理cookie
import scrapy
from xiaohuaPro.items import XiaohuaproItem
# http://www.521609.com/meinvxiaohua/list12%d.html
class XiaohuaSpider(scrapy.Spider):
name = 'xiaohua'
# allowed_domains = ['www.xxx.com']
start_urls = ['http://www.521609.com/meinvxiaohua/']
# 通用的url模板(不可变)
url = 'http://www.521609.com/meinvxiaohua/list12%d.html'
page_num = 2
# 该方法什么时候被调用: post 请求
# def start_requests(self):
# for url in self.start_urls:
# yield scrapy.FormRequest(url,formdata={},callback=self.parse)
def parse(self, response):
print('正在爬取第{}页的数据......'.format(self.page_num))
li_list = response.xpath('//*[@id="content"]/div[2]/div[2]/ul/li')
for li in li_list:
img_name = li.xpath('./a[2]/text() | ./a[2]/b/text()').extract_first()
item = XiaohuaproItem() # 封装到item中
item['img_name'] = img_name
yield item # 生成数据
# 递归函数的终止条件
if self.page_num <= 11:
new_url = format(self.url % self.page_num)
self.page_num += 1
# 手动请求发送
yield scrapy.Request(url=new_url, callback=self.parse) # 回调函数
item程序
import scrapyclass XiaohuaproItem(scrapy.Item): # define the fields for your item here like: img_name = scrapy.Field()
回顾
-
基本使用
- 新建工程
scrapy startproject proname
- cd proname
- 创建爬虫文件 scrapy genspider spiderName www.xxx.com
- 爬虫类:
- 父类:Spider
- name
- start_urls start_requests()让起始url有了自动发送请求的能力
- parse(self, reponse)
- 父类:Spider
- 爬虫类:
- 执行: scrapy crawl spiderName
- 新建工程
-
数据解析
- response.xpath()
- scrapy的xpath和etree的xpath区别:
- xpath返回的列表元素的类型不一样
- extract() extract_first()
-
持久化存储
- 基于终端指令
- 只可以将parse方法的返回值进行本地文件的持久化存储
scrapy crawl spiderName -o filePath
- 局限性比较强
- 基于管道(编码流程)
- 数据解析
- 定义item类(定义相关的属性 Field)
- 将解析的数据封装或存储到item类型的对象中
- 将item对象提交给管道 yield item
- 管道中实现process_item方法,让其接受item且对item进行持久化存储
- 将管道开启settings
- 细节处理
- 管道文件中的类代表表示什么含义?
- 一个管道类对应一种持久化存储的方式
- return item:将item传递给下一个即将被执行的管道类
- 管道文件中的类代表表示什么含义?
- 基于终端指令
-
settings.py
- 关闭robots
- 进行
UA
伪装 - 开启管道
- 定制日志类型
-
基于Spider全传数据爬取
- 定制一个通用的
url
模板 - 手动的对指定
url
进行请求发送:yield scrapy.Request(url,callback)
- 定制一个通用的
-
post请求:
yield scrapy.FormRequest(url,formdata,callback)
scrapy
自动开启了cookie处理
案例二:scrapy
爬取糗事糗图持久化存储
爬虫文件
import scrapy
from qiushiPic.items import QiushipicItem
class PictureSpider(scrapy.Spider):
name = 'picture'
# allowed_domains = ['www.baidu.com'] # 允许通过的域名 一般都注释
start_urls = ['https://www.qiushibaike.com/pic/']
# 通用的url模板
url = 'https://www.qiushibaike.com/pic/page/%d/'
page_num = 2
def parse(self, response):
print('正在爬取第{}页的数据......'.format(self.page_num - 1))
div_list = response.xpath('//div[@id="content-left"]/div')
for div in div_list:
img_url = 'https:'+div.xpath('./div[2]/a/img/@src').extract_first() # 获取图片url地址
# print(img_url)
item = QiushipicItem()
item['img_url'] = img_url
yield item # 封装
# 递归函数的终止条件
if self.page_num <= 12:
new_url = format(self.url % self.page_num)
self.page_num += 1
# 手动请求发送
yield scrapy.Request(url=new_url, callback=self.parse) # 回调函数
items文件
class QiushipicItem(scrapy.Item):
img_url = scrapy.Field() # 存储图片url地址
pipelines文件
对于图片的爬取来说,管道类我们有封装好得,继承ImagesPipeline
,继承他即可,然后在定义方法
import scrapy
from scrapy.pipelines.images import ImagesPipeline
class ImgproPipeline(object):
def process_item(self, item, spider):
return item
# 定制指定父类的管道类
class ImgPileline(ImagesPipeline):
# 根据图片地址进行图片数据的请求
def get_media_requests(self, item, info):
# 不需要指定回调函数
yield scrapy.Request(url=item['img_url'])
# 指定图片存储的名称
def file_path(self, request, response=None, info=None):
url = request.url # 图片地址
name = url.split('/')[-1]
return name
# 将item传递给下一个即将被执行的管道类
def item_completed(self, results, item, info):
return item
五大核心组件
-
每一个组件的作用
-
组件之间的工作流程
引擎(Scrapy)
用来处理整个系统的数据流处理, 触发事务(框架核心)
调度器(Scheduler)
用来接受引擎发过来的请求, 压入队列中, 并在引擎再次请求的时候返回. 可以想像成一个URL(抓取网页的网址或者说是链接)的优先队列, 由它来决定下一个要抓取的网址是什么, 同时去除重复的网址
下载器(Downloader)
用于下载网页内容, 并将网页内容返回给蜘蛛(Scrapy下载器是建立在twisted这个高效的异步模型上的)
爬虫(Spiders)
爬虫是主要干活的, 用于从特定的网页中提取自己需要的信息, 即所谓的实体(Item)。用户也可以从中提取出链接,让Scrapy继续抓取下一个页面
项目管道(Pipeline)
负责处理爬虫从网页中抽取的实体,主要的功能是持久化实体、验证实体的有效性、清除不需要的信息。当页面被爬虫解析后,将被发送到项目管道,并经过几个特定的次序处理数据。 -
请求转发(深度爬取)
-
使用场景:
- 爬取的数据不在同一张页面中
-
Request(url,callback,meta={xx:xx}):meta就可以传递个callback
-
callback接受meta:response.meta
-
-
适当提升scrapy爬取数据的效率:
增加并发:
默认scrapy开启的并发线程为32个,可以适当进行增加。在settings配置文件中修改CONCURRENT_REQUESTS = 100值为100,并发设置成了为100。降低日志级别:
在运行scrapy时,会有大量日志信息的输出,为了减少CPU的使用率。可以设置log输出信息为INFO或者ERROR即可。在配置文件中编写:LOG_LEVEL = ‘INFO’禁止cookie:
如果不是真的需要cookie,则在scrapy爬取数据时可以禁止cookie从而减少CPU的使用率,提升爬取效率。在配置文件中编写:COOKIES_ENABLED = False禁止重试:
对失败的HTTP进行重新请求(重试)会减慢爬取速度,因此可以禁止重试。在配置文件中编写:RETRY_ENABLED = False减少下载超时:
如果对一个非常慢的链接进行爬取,减少下载超时可以能让卡住的链接快速被放弃,从而提升效率。在配置文件中进行编写:DOWNLOAD_TIMEOUT = 10 超时时间为10s -
中间件
-
下载中间件:拦截请求和响应
-
爬虫中间件
-
拦截请求
- UA伪装:process_request
- 设置代理:process_exception,return request
-
拦截响应
- 篡改响应对象或者响应数据
- 符合需求的响应数据:selenium
- scrapy结合selenium的使用
- 实例化一个浏览器对象:
- 爬虫类的init中进行实例化操作
- 爬虫类的closed(self,spider)进行浏览器对象的关闭
- 需要将浏览器对象提交给下载中间件的process_response方法
- 在process_response中通过代码指定相关的行为动作
- 实例化一个浏览器对象:
- scrapy结合selenium的使用
-
-
scrapy处理动态加载的数据
中间件使用
settings文件
BOT_NAME = 'midllePro'
LOG_LEVEL = 'ERROR'
ROBOTSTXT_OBEY = False
DOWNLOADER_MIDDLEWARES = {
'midllePro.middlewares.MidlleproDownloaderMiddleware': 543,
}
middlewares文件
import random
class MidlleproDownloaderMiddleware(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",
"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"
]
# ip池
PROXY_http = [
'153.180.102.104:80',
'195.208.131.189:56055',
]
PROXY_https = [
'120.83.49.90:9000',
'95.189.112.214:35508',
]
# 作用:拦截正常请求
def process_request(self, request, spider):
print('this is process_request!!!')
# 进行UA伪装
request.headers['User-Agent'] = random.choice(self.user_agent_list)
# 测试
request.meta['proxy'] = 'http://27.208.92.247:8060'
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):
# 代理ip的设置
if request.url.split(':')[0] == 'http':
request.meta['proxy'] = random.choice(self.PROXY_http)
else:
request.meta['proxy'] = random.choice(self.PROXY_https)
return request # 将修正之后的异常请求进行重新发送
爬虫文件
class MiddleSpider(scrapy.Spider):
name = 'middle'
# allowed_domains = ['www.xxx.com']
start_urls = ['http://www.baidu.com/s?ie=UTF-8&wd=ip']
def parse(self, response):
page_text = response.text
with open('./ip.html', 'w', encoding='utf-8') as fp:
fp.write(page_text)
案例:请求转发(深度爬取)爬取视频名和详细
settings.py
USER_AGENT = 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36'
ROBOTSTXT_OBEY = False
LOG_LEVEL = 'ERROR'
ITEM_PIPELINES = {
'MoviePro.pipelines.MovieproPipeline': 300,
}
spider文件
import scrapy
from ..items import MovieproItem
class MovieSpider(scrapy.Spider):
name = 'movie'
# allowed_domains = ['www.baidu.com']
msg = input('电影类型:')
start_urls = [f'https://www.4567tv.tv/index.php/vod/show/class/{msg}/id/1.html']
url = f'https://www.4567tv.tv/index.php/vod/show/class/{msg}/id/%d.html'
page_num = 2
def parse(self, response):
li_list = response.xpath('/html/body/div[1]/div/div/div/div[2]/ul/li') # 解析页面的li
for li in li_list:
# 解析到电影的名字和详情页的url地址
name = li.xpath('./div/a/@title').extract_first()
detail_url = 'https://www.4567tv.tv' + li.xpath('./div/a/@href').extract_first()
# print(name,detail_url)
item = MovieproItem()
item['name'] = name
# meta是一个字典,可以将meta传递给callback
yield scrapy.Request(detail_url, callback=self.desc_prase, meta={'item': item})
if self.page_num <= 5:
new_url = format(self.url % self.page_num)
self.page_num += 1
yield scrapy.Request(new_url, callback=self.parse)
# 详情简介处理
def desc_prase(self, response):
desc = response.xpath('/html/body/div[1]/div/div/div/div[2]/p[5]/span[2]/text()').extract_first()
item = response.meta['item'] # callback 回去的item
item['desc'] = desc
yield item
items.py
class MovieproItem(scrapy.Item):
# define the fields for your item here like:
name = scrapy.Field()
desc = scrapy.Field()
pipelines.py
class MovieproPipeline(object):
def process_item(self, item, spider):
print(item)
return item
案例2:爬取网易新闻基于selenium和scrapy中间件存入数据库
settings文件
BOT_NAME = 'wangyiPro'
SPIDER_MODULES = ['wangyiPro.spiders']
NEWSPIDER_MODULE = 'wangyiPro.spiders'
USER_AGENT = 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36'
ROBOTSTXT_OBEY = False
LOG_LEVEL = 'ERROR'
# 开启下载器的中间件
DOWNLOADER_MIDDLEWARES = {
'wangyiPro.middlewares.WangyiproDownloaderMiddleware': 543,
}
ITEM_PIPELINES = {
'wangyiPro.pipelines.WangyiproPipeline': 300,
}
spider文件
import scrapy
from ..items import WangyiproItem
from selenium import webdriver
class WangyiSpider(scrapy.Spider):
name = 'wangyi'
start_urls = ['https://news.163.com/']
# 存储的是5个板块对应的url
five_model_urls = []
# 开启selenium对象
def __init__(self):
self.bro = webdriver.Chrome(executable_path='F:\spiderlearn\chromedriver.exe')
# 从网易新闻首页中解析出来5个板块 对应的url
def parse(self, response):
li_list = response.xpath('//*[@id="index2016_wrap"]/div[1]/div[2]/div[2]/div[2]/div[2]/div/ul/li')
alist = [3, 4, 6, 7, 8] # 5个板块的li标签
for a in alist:
li = li_list[a] # 5个板块对应的li
# 五个板块对应详情页的url
news_url = li.xpath('./a/@href').extract_first()
self.five_model_urls.append(news_url)
# 对五个板块详情页发起请求
yield scrapy.Request(news_url, callback=self.new_parse)
# response就是五个板块对应的响应对象
# 响应对象中的响应数据是不包含动态加载加载的新闻数据的
def new_parse(self, response):
# 解析每一个板块中的新闻数据
div_list = response.xpath('/html/body/div/div[3]/div[4]/div[1]/div/div/ul/li/div/div')
# print(div_list)
for div in div_list:
# 解析新闻标题和详情页的url
title = div.xpath('./div/div[1]/h3/a/text()').extract_first() # 新闻标题
detail_url = div.xpath('./div/div[1]/h3/a/@href').extract_first() # 详情页url
key_words = div.xpath('./div/div[2]/div//text()').extract() # 标签 分类
key_words = ''.join(key_words)
# print(title,key_words,detail_url)
if detail_url is not None:
item = WangyiproItem()
item['title'] = title
item['key_words'] = key_words
# 对新闻详情页发起请求获取新闻数据 meta将参数传递给回调函数
yield scrapy.Request(detail_url, callback=self.detail_parse, meta={'item': item})
# 解析详情页数据
def detail_parse(self, response):
item = response.meta['item']
content = response.xpath('//*[@id="endText"]//text()').extract() # 解析到详情数据列表
content = ''.join(content)
item['content'] = content
yield item # 提交给管道
# selenium中关闭浏览器
def closed(self, spider):
self.bro.quit()
items文件
import scrapy
class WangyiproItem(scrapy.Item):
title = scrapy.Field()
content = scrapy.Field()
key_words = scrapy.Field()
管道文件
class WangyiproPipeline(object):
def process_item(self, item, spider):
print(item)
return item
import pymysql
class mysqlPileLine(object):
conn = None
cursor = None
def open_spider(self, spider):
# 连接数据库
self.conn = pymysql.Connect(host='127.0.0.1', port=3306, user='root', password='root', db='wangyi',
charset='utf8')
# # 执行SQL语句 接下来创建游标对象 游标是可以多次创建的
# self.cursor = self.conn.cursor()
print(self.conn)
def process_item(self, item, spider):
sql = 'insert into qiubai values ("{}","{}","{}")'.format(item['title'], item['content'],item['key_words'])
# 执行SQL语句 接下来创建游标对象 游标是可以多次创建的
self.cursor = self.conn.cursor()
# 进行事务处理
try:
self.cursor.execute(sql)
# 提交数据
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()
中间件middlewares
from scrapy import signals
from time import sleep
from scrapy.http import HtmlResponse
class WangyiproDownloaderMiddleware(object):
def process_request(self, request, spider):
return None
# 拦截响应(1+5+n个响应)
def process_response(self, request, response, spider):
# 该方法会拦截到所有的响应(1+5+n)
# 我们需要篡改的是五个板块对应的响应对象
# 如何有针对性的捕获到五个板块对应的响应对象
# 定位指定响应对象的方法:
# 根据url定位到指定的request
# 根据指定的request定位到指定的response
urls = spider.five_model_urls
if request.url in urls:
# 将原始不满足需求的response篡改成符合需求的新的response
# 先要获取符合需求的响应数据,然后将该响应数据封装到新的响应对象中,将新响应对象返回
bro = spider.bro
bro.get(request.url)
sleep(2)
js = 'window.scrollTo(0,document.body.scrollHeight)'
bro.execute_script(js)
sleep(1)
bro.execute_script(js)
sleep(1)
bro.execute_script(js)
sleep(1)
# 返回的页面源码就包含了动态加载的新闻数据,page_text是需要作为新的响应对象的响应数据
page_text = bro.page_source
new_response = HtmlResponse(url=bro.current_url, body=page_text, encoding='utf-8', request=request)
return new_response
else:
return response
def process_exception(self, request, exception, spider):
pass
🕠new
- 中间件
- 批量拦截请求和响应
- process_request(request,spider):拦截请求
- process_response(request,response,spider):拦截所有响应
- process_exception(request,exception,spider):拦截异常的请求,return request
- 拦截请求:
- UA伪装:process_request:request.headers['User-Agent']='xxx'
- 代理设置:process_exception:request.meta['proxy']='http://ip:port'
- 拦截响应:
- 篡改响应数据或者响应对象
- sometimes,获取的相应对象是不满足需求的要求。
- HTMLResponse(url,body,request,encoding) # body import
- 批量拦截请求和响应
- 请求转发:
- 应用场景:实现数据的深度爬取(爬取的数据没有在同一张页面中)
- why?不同深度的页面,页面布局是不一样的。进行数据解析的方法也需要多个。多个不同的解析方法需要使用同一个item类型对象。就需要使用请求传参将item进行传递
- Request(url,callback,meta={'item':item}) meta:就是传递给了callback函数
- callback: item = response.meta['item']
CrawlSpider 爬取案例
CrawlSpider
是Spider的一个子类
-
作用:专门用作于全栈数据爬取
-
使用流程:
- 创建工程
scrapy startproject proname
- cd 切换目录,之后创建爬虫文件
scrapy genspider -t crawl spiderName www.xxx.com
- 执行
- 创建工程
-
重要功能:
-
连接提取器:
LinkExtractor:根据指定的规则(allow=‘正则’)进行连接的提取
-
规则解析器:
连接提取器是被作用在规则解析器中。将连接提取器提取到的连接进行请求发送,然后使用指定的规则(callback)对响应数据进行数据解析。
-
注意:一个连接提取器一定对应唯一的一个规则解析器
-
-
基于
CrawlSpider
进行深度数据爬取
settings文件
BOT_NAME = 'SunlinePro'
SPIDER_MODULES = ['SunlinePro.spiders']
NEWSPIDER_MODULE = 'SunlinePro.spiders'
USER_AGENT = 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36'
ROBOTSTXT_OBEY = False
LOG_LEVEL = 'ERROR'
ITEM_PIPELINES = {
'SunlinePro.pipelines.SunlineproPipeline': 300,
}
items文件
import scrapy
class SunlineproItem(scrapy.Item):
number = scrapy.Field()
title = scrapy.Field()
status = scrapy.Field()
class ContentItem(scrapy.Item):
number = scrapy.Field()
content = scrapy.Field()
CrawlSpider文件
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from ..items import SunlineproItem, ContentItem
class SunlineSpider(CrawlSpider):
name = 'sunline'
start_urls = ['http://wz.sun0769.com/index.php/question/questionType?type=4&page=']
# 实例化了一个链接提取器对象,其作用是勇于连接符合制定规则的提取
link = LinkExtractor(allow=r'type=4&page=\d+') # 正则
# link_all = LinkExtractor(allow=r'') # 提取所有的连接
# 提取详情页的url
link_detail = LinkExtractor(allow=r'question/\d+/\d+\.shtml')
rules = (
# 实例化了一个规则解析器对象 follow为TRUE是解析所有界面 FALSE是当前页面
Rule(link, callback='parse_item', follow=True),
Rule(link_detail, callback='parse_detail', follow=True),
)
# 解析标题和状态
def parse_item(self, response):
# xpath表达式中如果出现了tbody标签必须跨过
tr_list = response.xpath('//*[@id="morelist"]/div/table[2]//tr/td/table//tr')
for tr in tr_list:
number = tr.xpath('./td/text()').extract_first()
title = tr.xpath('./td[2]/a[2]/text()').extract_first()
status = tr.xpath('./td[3]/span/text()').extract_first()
item = SunlineproItem()
item['number'] = number
item['title'] = title
item['status'] = status
yield item
# 解析新闻内容
def parse_detail(self, response):
number = response.xpath('/html/body/div[9]/table[1]//tr/td[2]/span[2]/text()').extract_first()
try:
number = number.split(':')[-1]
except AttributeError:
print('出现异常!!')
content = response.xpath('/html/body/div[9]/table[2]//tr[1]/td//text()').extract()
content = ''.join(content)
item = ContentItem()
item['number'] = number
item['content'] = content
# print(number)
yield item
pipelines文件
class SunlineproPipeline(object):
def process_item(self, item, spider):
dic = {}
# 判断接受到的item到底是哪一个item
if item.__class__.__name__ == 'ContentItem':
content = item['content']
num = item['number']
dic['content'] = content
else:
number = item['number']
title = item['title']
status = item['status']
return item
分布式爬虫
组建一个分布式集群,然后让其共同执行同一组程序,实现数据的分布式爬取
如何实现分布式?
scrapy-redis
组件结合原生的scrapy实现分布式
原生的scrapy为什么不能实现分布式?
- 无法共享一个调度器
- 无法共享一个管道
scrapy-redis
的作用:
- 可以给scrapy提供共享的调度器和管道,但是他只能连接Redis数据库,对于管道来说,是不可修改的
1. 分布式编码流程:
-
工程的创建
-
创建爬虫文件 spider CrawlSpider
-
修改爬虫文件:
- 导包:
from scrapy_redis.spiders import RedisCrawlSpider
- 将爬虫类的父类修改成RedisCrawlSpider
- 将allow_demains和start_urls删除
- 添加一个新属性:redis_key='xxx':调度器队列的名称
- 完善爬虫类的相关代码(连接提取器,规则解析器,解析方法)
- 导包:
-
配置文件的修改:
-
指定管道
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_HOST = 'redis服务的ip地址' REDIS_PORT = 6379
-
-
修改Redis的配置:redis.windows.conf
- 关闭默认绑定: # bind 127.0.0.1
- 关闭保护模式:protected-mode no
-
结合配置文件启动Redis服务,启动客户端
redis-server ./redis.windows.conf
redis-cli
-
启动执行分布式工程:
scrapy crawl fbs
orscrapy runspider ./xxx.py
-
向调度器队列中扔入一个起始url:
- 调度器的队列是存放在Redis中
- 在redis-cli:lpush redis_key的值 www.xxx.com
2. 分布式爬虫案例1-阳光热线
http://wz.sun0769.com/index.php/question/questionType?type=4&page=
settings.py
BOT_NAME = 'fbsPro'
SPIDER_MODULES = ['fbsPro.spiders']
NEWSPIDER_MODULE = 'fbsPro.spiders'
USER_AGENT = 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36'
ROBOTSTXT_OBEY = False
CONCURRENT_REQUESTS = 32
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_HOST = '127.0.0.1'
REDIS_PORT = 6379
REDIS_ENCODING = 'utf-8'
items.py
import scrapy
class FbsproItem(scrapy.Item):
title = scrapy.Field()
status = scrapy.Field()
spiders文件
from scrapy_redis.spiders import RedisCrawlSpider
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from ..items import FbsproItem
class FbsSpider(RedisCrawlSpider):
name = 'fbs'
redis_key = 'sun' # 调度器队列的名称
rules = (
Rule(LinkExtractor(allow=r'type=4&page=\d+'), callback='parse_item', follow=True),
)
def parse_item(self, response):
tr_list = response.xpath('//*[@id="morelist"]/div/table[2]//tr/td/table//tr')
for tr in tr_list:
title = tr.xpath('./td[2]/a[2]/text()').extract_first()
status = tr.xpath('./td[3]/span/text()').extract_first()
item = FbsproItem()
item['title'] = title
item['status'] = status
yield item
增量式
监测网站数据更新的情况。 重心:去重
案例:爬取视频标题和简介—持久化存入Redis
spider文件
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from redis import Redis
from ..items import ZlsMovieProItem
class ZlsSpider(CrawlSpider):
name = 'zls'
start_urls = ['https://www.4567tv.tv/index.php/vod/show/class/喜剧/id/1.html']
rules = (
Rule(LinkExtractor(allow=r'page/\d+\.html'), callback='parse_item', follow=True),
)
conn = Redis()
# 解析出电影的名称
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:
title = li.xpath('./div/a/@title').extract_first()
detail_url = 'https://www.4567tv.tv' + li.xpath('./div/a/@href').extract_first()
item = ZlsMovieProItem()
item['title'] = title
# 将detail_url全部存储到redis的set中
ex = self.conn.sadd('movie_detail_urls', detail_url)
if ex == 1: # detail_url不曾存在于redis
print('有最新更新的数据可爬......')
# 对详情页发起请求爬取电影的简介
yield scrapy.Request(detail_url, callback=self.parse_detail, meta={'item': item})
else:
print('暂无数据更新!!!')
# 解析电影的简介
def parse_detail(self, response):
item = response.meta['item']
item['desc'] = response.xpath('/html/body/div[1]/div/div/div/div[2]/p[5]/span[2]/text()').extract_first()
yield item
items.py
import scrapy
class ZlsMovieProItem(scrapy.Item):
title = scrapy.Field()
desc = scrapy.Field()
pipelines.py
class ZlsMovieProPipeline(object):
def process_item(self, item, spider):
conn = spider.conn
conn.lpush('movie_data', item)
print(item)
return item
settings.py
BOT_NAME = 'zls_movie_Pro'
SPIDER_MODULES = ['zls_movie_Pro.spiders']
NEWSPIDER_MODULE = 'zls_movie_Pro.spiders'
USER_AGENT = 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36'
LOG_LEVEL = 'ERROR'
ROBOTSTXT_OBEY = False
ITEM_PIPELINES = {
'zls_movie_Pro.pipelines.ZlsMovieProPipeline': 300,
}
快讯新闻爬取
import requests
import re
import json
url = 'http://newsapi.eastmoney.com/kuaixun/v1/getlist_103_ajaxResult_50_%d_.html'
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36",
}
for i in range(1,21):
new_url = format(url%i) # 新的url
# 获取文本文件
page_text = requests.get(url=new_url,headers=headers).text
# 进行正则匹配字典字符串
page_str = re.findall('\{.*\}',page_text)[0]
# 序列化字符串
page_dic = json.loads(page_str)
page_list = page_dic['LivesList']
content = []
for dic in page_list:
digest = dic['digest']
content.append(digest)
with open(f'./第{i}页新闻.txt','w',encoding='utf8') as f:
f.write('\n'.join(content))
print(f'第{i}页下载成功!')
github模拟登陆
import requests
from lxml import etree
class Login(object):
def __init__(self):
self.headers = {
"Referer": "https://github.com/",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"Host": "github.com",
}
self.login_url = "https://github.com/login"
self.post_url = "https://github.com/session"
self.session = requests.Session()
def token(self):
response = self.session.get(self.login_url, headers=self.headers)
selector = etree.HTML(response.text)
token = selector.xpath('//div//input[2]/@value')[0]
print(selector)
return token
def login(self, email, password):
post_data = {
"commit": "Sign in",
"utf8": "✓",
"authenticity_token": self.token(),
"login": email,
"password": password,
}
response = self.session.post(self.post_url, data=post_data, headers=self.headers)
if response.status_code == 200:
print("success")
page_text = response.text
with open('./login_main.html', 'w', encoding='utf8') as f:
f.write(page_text)
if __name__ == "__main__":
login = Login()
login.login(email='msd.yze@gmail.com', password='111111')
1. 简述cookie的概念和作用
答:cookie就是服务器保存在浏览器本地中的记录我们信息的一组键值对,他的作用是在登录或者注册的时候记录用户的状态。
2. 简述scrapy各个核心组件之间的工作流程
答:spider中的url被封装成请求对象交给引擎;引擎拿到对象后,将其全部交给调度器,调度器拿到所有的请求对象后在内部通过过滤器过滤掉重复的url,最后将去重后的url对应的请求对象压入队列中,之后调度器调度出其中的一个请求对象,并将其交个引擎,引擎将调度器调度出的请求对象交给下载器,下载器拿到请求对象去互联网中下载数据,数据下载完成后悔封装到response中,response交给下载器,下载器将response交给引擎,引擎将response交给spider,spider拿到response后调用回调方法进行数据解析,解析成功后产生item,接着spider将item交给引擎,引擎将item交给管道,管道拿到item后进行数据的持久化存储。
3. 基于crwalSpider实现数据爬取的流程
1. 创建工程 scrapy startproject proname
2. Cd切换目录,之后创建爬虫文件 scrapy genspider -t crawl spiderName www.baidu.com
3. 执行
4. 在scrapy中如何实现将同一份数据值存储到不同的数据库中
答:创建不同的管道类,每个管道类中实现一种存储方法,并在settings中注册。
5. scrapy的下载中间件的作用以及类中重点方法的使用介绍
答:批量拦截请求和响应,UA伪装:process_request: request.headers['User-Agent']=’xxxx’
代理设置process_exception:request.meta['proxy']='http://ip:port',
拦截响应,篡改响应数据或者相应对象。
6. scrapy的pipeline的作用及其工作原理
答:用于持久化数据,将引擎发来的item进行数据的持久化存储。
7. 有关scrapy的pipeline中的process_item方法的返回值有什么注意事项。
答:其返回值可以传递给下一个需要处理的对象
8. scrapy实现持久化存储有几种方式,如何实现
基于终端,可以将parse方法的返回值存储到本地磁盘文件中;基于管道,数据解析,在item类中进行相关属性的定义,将解析的数据封装到item类型的对象中,将item对象提交给管道,管道接受item然后调用管道类中的process_item方法进行数据的持久化存储。
9. 描述使用xpath实现数据解析的流程
答:实例化一个etree的对象,将即将被解析的页面加载到该对象中,调用etree对象中的xpath方法结合不用的xpath表达式实现标签定位 和数据提取。
10. 你如何处理相关动态加载的页面数据
答:1.利用selenium可以非常便捷的获取动态加载数据
2.借助于抓包工具可以进行分析和动态加载数据对应url的提取
11. 如何实现分布式?简述其实现和部署流程
Scrapy-redis组件结合原生的scrapy实现分布式
部署流程:
工程的创建
创建爬虫文件
修改爬虫文件,
导包
将爬虫类的父类改成RedisCrawlSpider
将allow_demains和start_urls删除
添加一个新的属性:redis_key=’xxx’:调度器队列的名称
完善爬虫类的相关代码(连接提取器,规则解析器,解析方法)
修改配置文件
指定管道
指定调度器
指定数据库
修改Redis的配置
启动Redis服务器,启动客户端;启动执行分布式工程
想调度器队列中仍如一个起始url
12. 谈谈你对https数据加密方式的理解
对称加密:有一个密钥,他可以对一段内容进行加密,加密后只能用它进行解密
非对称加密:有两把密钥,一把叫做公钥一把叫做私钥,公钥加密的数据只能用私钥打开,同样私钥加密的数据只能用公钥解开。
数字证书:网站在使用HTTPS前,需要向CA机构申请颁发一份数字证书,服务器把证书传输给浏览器,浏览器从中获取公钥 信息,证书就像是一个身份证。
数字签名:把证书内容生成一份签名,比对证书内容和签名是否一致就能察觉是否证书被篡改。主要是对证书中明文进行hash,hash后的值用私钥加密得到数字签名。
13. 原生的scrapy框架为什么不可以实现分布式?
无法共享一个调度器
无法共享一个管道
14. 常见的反爬机制有哪些?如何进行处理?
Headers:把headers传送给requests,绕过他
ip限制:根据ip地址的访问频率,次数进行反爬。构建自己的ip代理池,然后每次访问的时候随机选择代理
UA限制:浏览器标识。构建自己的UA池,每次请求是随机 选择UA标识
验证码,模拟登陆:验证码识别,打码平台
Ajax动态加载:抓包工具,selenium解决
15. 在爬虫中如何实现数据清洗(三种清洗方法)
去重,去除无效值和缺失值。