1.1 概念及安装
scrapy是基于异步模块twisted的爬虫框架,集成了爬虫项目中通用性较高的部分功能,具备高性能的数据解析,请求发送,持久化存储,全站数据爬取,中间件,分布式等
环境的安装:
- mac、linum:pip install scrapy
- windows:
- a. pip3 install wheel
- b. 下载twisted文件,下载地址:http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
- c. 进入下载目录,执行 pip install Twisted‑17.1.0‑cp35‑cp35m‑win_amd64.whl
- Twisted:就是一个异步的架构。被作用在了scrapy中。
- 安装报错:需要更换另一个版本的twisted文件进行安装即可。
- d. pip install pywin32
- e. pip install scrapy
- 测试:cmd中scrapy按下回车,如果没有报错说明安装成功。
1.2 scrapy的组件及工作流程
引擎(Scrapy):
驱动整个系统的运行,处理其他组件的数据流,触发事务(框架核心);
调度器(Scheduler):
接受引擎发过来的请求, 压入队列中, 并在引擎再次请求的时候返回. 可以想像成一个URL(抓取网页的网址或者说是链接)的优先队列, 由它来决定下一个要抓取的网址是什么, 同时去除重复的网址;
下载器(Downloader):
接收调度器经引擎发送的下载请求,完成从网络获取数据的任务(Scrapy下载器是建立在twisted这个高效的异步模型上的);
爬虫(Spiders):
处理主要的业务,决定爬取的内容,发送地址url给到引擎,接收下载器获取的数据,主要定制部分;
项目管道(Pipeline):
负责处理爬虫从网页中抽取的实体,主要的功能是持久化实体、验证实体的有效性、清除不需要的信息。当页面被爬虫解析后,将被发送到项目管道,并经过几个特定的次序处理数据,也是主要定制的组件;
中间件(Middlewares):
下载器中间件可扩展发送请求过程的规则,如添加ip代理,ua池等,
爬虫中间件可修改响应,基本无用处。
创建项目:
scrapy startproject testproject
创建爬虫:
cd testproject
scrapy gen testspider "www.test.com"
spider代码写入获取数据:
pipline代码写入保存数据:
数据建模Item写入获取的信息:
运行代码:
scrapy crawl testspider
1.4 定制组件案例
下载器中间件:
设置ua池及ip代理池
import random
import requests
import base64
import json
# UA池
class UserAgentDownloadMiddleware(object):
USER_AGENT = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36",
"Mozilla/5.0 (X11; Linux i686; rv:64.0) Gecko/20100101 Firefox/64.0",
"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:64.0) Gecko/20100101 Firefox/64.0",
"Mozilla/5.0 (X11; Linux i586; rv:63.0) Gecko/20100101 Firefox/63.0",
"Mozilla/5.0 (Windows NT 6.2; WOW64; rv:63.0) Gecko/20100101 Firefox/63.0"
]
def process_request(self,request,spider):
user_agent=random.choice(self.USER_AGENT)
request.headers['User-Agent']=user_agent
# IP proxy池
class IpPoxyDownloadMiddleware(object):
# 开放代理
PROXIES = [
"178.44.170.152:8080", "110.44.113.182:8080",
"209.126.124.73:8888", "84.42.79.243:8080",
"117.97.31.180:8080", "103.76.199.166:8080"]
def process_request(self, request, spider):
proxy=random.choice(self.PROXIES)
request.meta['proxy']=proxy
# 独享代理
PROXIES_URL=""
def process_request(self, request, spider):
proxy = '121.199.6.124:16816'
user_password = ""
request.meta['proxy'] = proxy
# bytes
b64_user_password = base64.b64encode(user_password.encode('utf-8'))
# 设置认证
request.headers['Prox-Authorzation'] = 'Basic' + b64_user_password.decode('utf-8')
def process_request(self,request,spider):
pass
def process_response(self,request,response,spider):
pass
def get_proxy(self):
response=requests.get(self.PROXIES_URL)
text=response.text
result=json.loads(text)
data=result['data'][0]
使用selenium修改下载中间件处理ajax数据,也可通过selenium登录获取cookie:
from selenium import webdriver
import time
from scrapy.http.response.html import HtmlResponse
class SeleniumDownloadMiddleware(object):
def __init__(self):
# 实例化driver对象
self.driver=webdriver.Chrome(executable_path=r"")
def process_request(self,request,spider):
# 对起始url发起请求
self.driver.get(request.url)
time.sleep(1)
try:
while True:
showmore=self.driver.find_element_by_class_name('show-more')
showmore.click()
time.sleep(0.3)
if not showmore:
break
except:
pass
# 获取源代码
source=self.driver.page_source
# 返回selenium请求后的响应
response=HtmlResponse(url=self.driver.current_url,body=source,request=request,encoding='utf-8')
return response
Item pipline:
存储方式:
import pymysql
# mysql同步存储
class JianshuSpiderPipeline(object):
def __init__(self):
dbparams={
'host':'172.0.0.1',
'port':3306,
'user':'root',
'password':'123456',
'charset':'utf8'
}
# 创建一个连接对象
self.conn=pymysql.connect(**dbparams)
# 获取游标
self.cursor=self.conn.cursor()
self._sql=None
def process_item(self, item, spider):
# 执行sql语句
self.cursor.execute(self.sql,(item['title'],item['content'],
item['author'],item['avatar'],item['pub_time'],item['origin_url'],item['article']))
# 确认
self.conn.commit()
return item
# 魔法方法,构建sql语句
@property
def sql(self):
if not self._sql:
self._sql="""
insert into article(id,title,content,author,avatar,pub_time,
origin_url,article_id) values(null,%s,%s,%s,%s,%s,%s,%s)
"""
return self._sql
return self._sql
from pymysql import cursors
from twisted.enterprise import adbapi
# mysql异步存储
class JianshuTwsitedSpiderPipeline(object):
def __init__(self):
dbparams={
'host':'172.0.0.1',
'port':3306,
'user':'root',
'password':'123456',
'charset':'utf8',
'cursorclass':cursors.DictCursor
}
# 创建连接池
self.dbpool=adbapi.ConnectionPool('pymysql',**dbparams)
self._sql=None
@property
def sql(self):
if not self._sql:
self._sql="""
insert into article(id,title,content,author,avatar,pub_time,
origin_url,article_id) values(null,%s,%s,%s,%s,%s,%s,%s)
"""
return self._sql
return self._sql
def process_item(self,item,spider):
# 启动连接池,异步插入sql语句
defer=self.dbpool.runInteraction(self.insert_item,item)
defer.addErrback(self.handler_error,item,spider)
def insert_item(self,cursor,item):
cursor.execute(self.spl,(item['title'],item['content'],
item['author'],item['avatar'],item['pub_time'],item['origin_url'],item['article']))
def handler_error(self,error,item,spider):
print("="*10+'error'+"="*10)
print(error)
print("=" * 10 + 'error' + "=" * 10)
# 以json格式存储数据
from scrapy.exporters import JsonLinesItemExporter
class BossSpiderPipeline(object):
def __init__(self,):
self.fp=open('boss.json','wb')
# 实例化一个写入对象
self.exporter=JsonLinesItemExporter(self.fp,ensure_ascii=False,encoding='utf-8')
def process_item(self, item, spider):
# 写入字典格式的数据
self.exporter.export_item(item)
return item
def close_spider(self,spider):
self.fp.close()
大文件存储:
from urllib import request
import os
from scrapy.pipelines.images import ImagesPipeline
from bmw_img import settings
class BmwImgPipeline(object):
def __init__(self):
# 构建图片保存的路径
self.path=os.path.join(os.path.dirname(os.path.dirname(__file__)),'images')
if not os.path.exists(self.path):
os.mkdir(self.path)
def process_item(self, item, spider):
category=item['category']
urls=item['urls']
# 构建分类保存路径
category_path=os.path.join(self.path,category)
if not os.path.exists(category_path):
os.mkdir(category_path)
for url in urls:
image_name=url.split('_')[-1]
# 下载保存在本地
request.urlretrieve(url,os.path.join(category_path,image_name))
return item
# 重写下载规则,实现分类保存图片
from . import settings
from scrapy.pipelines.images import ImagesPipeline
import os
import re
# 继承图片下载的pipline
class ZcoolPicturePipeline(ImagesPipeline):
def get_media_requests(self, item, info):
"""获取item并绑定在request上传递给file_path方法"""
# 获取所有请求
media_requests=super(ZcoolPicturePipeline, self).get_media_requests(item,info)
for media_request in media_requests:
# 将item绑定给request
media_request.item=item
return media_requests
def file_path(self, request, response=None, info=None):
"""重构保存路径,实现分类存储"""
# 原始pipline的存储路径
origin_path=super(ZcoolPicturePipeline,self).file_path(request,response,info)
title=request.item.get('title')
# 去掉文件夹中不允许存在的字符
title=re.sub(r'[\\/:\*\?"<>\|]',"",title)
save_path=os.path.join(settings.IMAGES_STORE,title)
if not os.path.exists(save_path):
os.mkdir(save_path)
# 去除原始存储路径得到文件名称
image_name=origin_path.replace("full/",'')
save_path=os.path.join(save_path,image_name)
2.1 crawl的使用案例
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from ..items import ZcoolPictureItem
class ZcoolSpider(CrawlSpider):
name = 'zcool'
allowed_domains = ['www.zcool.com.cn']
start_urls = ['https://www.zcool.com.cn/discover/0!0!0!0!0!!!!-1!0!1']
# 自动匹配出需要请求的url
rules = (
Rule(LinkExtractor(allow=r'.+0!0!0!0!0!!!!-1!0!\d+/'), follow=True),
Rule(LinkExtractor(allow=r'.+/work/.+html'),callback='parse_detail',follow=False)
)
# 提取数据
def parse_detail(self,response):
title=response.xpath("//div[@class='details-contitle-box']/h2//text()").getall()
title=''.join(title).strip()
img_url=response.xpath("//div[@class='photo-information-content']/img/@src").getall()
item=ZcoolPictureItem(title=title,img_url=img_url)
yield item
2.2 分布式爬虫介绍
需要搭建一个分布式的机群,让后让机群中的每一台电脑执行同一组程序,让其对同一组资源进行联合且分布的数据爬取。
实现方式:scrapy+redis(scrapy结合着scrapy-redis组件)
原生的scrapy框架因为无法共享调度器与管道所以不能实现分布式;
如何实现分布式:使用scrapy-redis组件即可,给原生的scrapy框架提供共享的管道和调度器;
实现流程:
导包:from scrapy_redis.spiders import RedisCrawlSpider
修改当前爬虫类的父类为:RedisCrawlSpider
将start_url替换成redis_keys的属性,属性值为任意字符串
redis_key = ‘xxx’:表示的是可以被共享的调度器队列的名称,最终是需要将起始的url手动放置到redis_key表示的队列中
对settings.py进行配置:
指定调度器:
# 增加了一个去重容器类的配置, 作用使用Redis的set集合来存储请求的指纹数据, 从而实现请求去重的持久化
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
# 使用scrapy-redis组件自己的调度器
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
# 配置调度器是否要持久化, 也就是当爬虫结束了, 要不要清空Redis中请求队列和去重指纹的set。如果是True, 就表示要持久化存储, 就不清空数据, 否则清空数据
SCHEDULER_PERSIST = True
指定管道:
ITEM_PIPELINES = {
'scrapy_redis.pipelines.RedisPipeline': 400
}
特点:该种管道只可以将item写入redis
指定redis:
REDIS_HOST = 'redis服务的ip地址'
REDIS_PORT = 6379
配置redis的配置文件(redis.window.conf):
解除默认绑定
56行:#bind 127.0.0.1
关闭保护模式
75行:protected-mode no
启动redis服务和客户端
执行scrapy工程(不要在配置文件中加入LOG_LEVEL)
程序会停留在listening位置:等待起始的url加入
向redis_key表示的队列中添加起始url
需要在redis的客户端执行如下指令:(调度器队列是存在于redis中)
lpush sunQueue http://wz.sun0769.com/political/index/politicsNewest?id=1&page=1
2.3 分布式实现案例
爬虫部分:
# -*- coding: utf-8 -*-
import scrapy
from ..items import LianjiaItem
import copy
# crawlspider继承RedisCrawlSpider
from scrapy_redis.spiders import RedisSpider
class LjSpiderSpider(RedisSpider):
name = 'lj_spider'
allowed_domains = ['www.lianjia.com']
# 分布式需要修改为redis_key作为起始钥匙
redis_key= 'lj'
def parse(self, response):
"""获取到每个城市的url"""
item = LianjiaItem()
pro_list = response.xpath("//div[@class='city_province']")
for pro in pro_list:
item['provience']= pro.xpath("./div/text()").get()
cities=pro.xpath("./ul//li//a")
for city in cities:
city_name=city.xpath("./text()").get()
city_url=city.xpath("./@href").get()
# print(item['provience'],item['city_name'],city_url)
item['city_name']=city_name
# print(city_dict)
# 发起get请求,meta参数需深拷贝防止数据被冲掉
yield scrapy.Request(url=city_url+"zufang",callback=self.get_area_url,meta={
'item':copy.deepcopy(item)},dont_filter=True)
def get_area_url(self,response):
"""获取每个城市每个区的url"""
item=response.meta.get('item')
# print(item)
area_list=response.xpath("//div[@class='filter']//ul[2]/li")[1:]
for area in area_list:
item['area_name']=area.xpath("./a/text()").get()
# area_name = area.xpath("./a/text()").get()
area_url=area.xpath("./a/@href").get()
url=response.urljoin(area_url)
yield item
设置部分:
# 修改调度器,确保request能够共享
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
# 使所有爬虫共享相同的去重指纹
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
# 设置redis为item pipeline,redis数据库作为数据存储的库
ITEM_PIPELINES = {
'scrapy_redis.pipelines.RedisPipeline': 300
}
# 保证所有进入对列的数据不被清理,可实现断点续爬
SCHEDULER_PERSIST = True
# 设置连接redis信息
REDIS_HOST = ''
REDIS_PORT = 6379
3.1 splash的应用
作用:scrapy-splash能够模拟浏览器加载js,并返回js运⾏后的数据
安装流程:
使用docker进行安装:splash-dockerfile=>拉取镜像:sudo docker pull scrapinghub/splash=>验证:前台: sudo docker run -p 8050:8050 scrapinghub/splash 后台: sudo docker run -d -p 8050:8050 scrapinghub/splash=>访问查看http://127.0.0.1:8050
配置:
# 渲染服务的url
SPLASH_URL = 'http://127.0.0.1:8050'
# 下载器中间件
DOWNLOADER_MIDDLEWARES = {
'scrapy_splash.SplashCookiesMiddleware': 723,
'scrapy_splash.SplashMiddleware': 725,
'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMi ddleware': 810, }
# 去重过滤器
DUPEFILTER_CLASS = 'scrapy_splash.SplashAwareDupeFilter'
# 使⽤Splash的Http缓存
HTTPCACHE_STORAGE = 'scrapy_splash.SplashAwareFSCacheStorage'
# Obey robots.txt rules
ROBOTSTXT_OBEY = False
使用案例:
import scrapy
from scrapy_splash import SplashRequest
# 使⽤scrapy_splash包提供的r equest对象
class WithSplashSpider(scrapy.Spider):
name = 'with_splash'
allowed_domains = ['baidu.com']
start_urls = ['https://www.baidu.com/s?wd=13161933309']
def start_requests(self):
yield SplashRequest(self.start_urls[0], callback=self.parse_splash, args={
'wait': 10}, endpoint='render.html') # 使⽤splash服务 的固定参数
def parse_splash(self, response):
with open('with_splash.html', 'w') as f:
f.write(response.body.decode())
3.2 scrapyd部署scrapy项⽬
介绍:scrapyd是⼀个⽤于部署和运⾏scrapy爬⾍的程序,它允许你通过JSON API 来部署爬⾍项⽬和控制爬⾍运⾏,scrapyd是⼀个守护进程,监听爬⾍的运⾏ 和请求,然后启动进程来执⾏它们。
安装:
scrapyd服务端: pip install scrapyd scrapyd
客户端: pip install scrapyd-client
启动:sudo scrapyd
访问:127.0.0.1:6800
部署:
在项目目录下找出scrapy.cfg文件
下载终端命令:curl命令
部署爬虫,爬虫根目录下执行:scrapyd-deploy 部署名 -p 项⽬名称
启动项⽬:
curl http://localhost:6800/schedule.json -d project=project_name -d spider=spider_name
关闭爬⾍:
curl http://localhost:6800/cancel.json -d project=project_name -d job=jobid
列出项⽬:
curl http://localhost:6800/listprojects.json
列出爬⾍:
curl http://localhost:6800/listspiders.json?project=myspider
列出job:
curl http://localhost:6800/listjobs.json?project=myspider
终⽌爬⾍(该功能会有延时或不能终⽌爬⾍的情况,此时 可⽤kill -9杀进程的⽅式中⽌):
curl http://localhost:6800/cancel.json -d project=myspider -d job=tencent
使用requests模块控制:
import requests
# 启动爬⾍
url = 'http://localhost:6800/schedule.json'
data = {
'project': 项⽬名,
'spider': 爬⾍名, }
resp = requests.post(url, data=data)
# 停⽌爬⾍
url = 'http://localhost:6800/cancel.json'
data = {
'project': 项⽬名,
'job': 启动爬⾍时返回的jobid, }
resp = requests.post(url, data=data)
3.3 Gerapy部署scrapy项目
安装Gerapy
pip install gerapy
初始化gerapy
gerapy init
初始化数据库
gerapy migrate
默认使用的是SQlite数据库
运行gerapy服务
gerapy runserver
访问Gerapy管理界面
http://127.0.0.1:8000
部署项目:
可直接将scrapy中的项目添加进去或者手动将爬虫文件移动到初始化包下的project文件夹