scrapy
Scrapy 内部集成了Twisted异步网络框架,可以加快我们的下载速度。
未使用scrapy框架之前的爬虫
使用之后
1 爬虫中起始的url构造成request对象-->爬虫中间件-->引擎-->调度器
2 调度器把request-->引擎-->下载中间件--->下载器
3 下载器给互联网发送请求,获取response响应---->下载中间件---->引擎--->爬虫中间件--->爬虫
4 爬虫提取url地址,组装成request对象---->爬虫中间件--->引擎--->调度器,重复步骤2
5 爬虫提取数据--->引擎--->管道处理和保存数据
图中绿色线条的表示数据的传递
注意图中中间件的位置,决定了其作用,中间件类似于请求钩子,把请求拦截,做处理再返回
注意其中引擎的位置,所有的模块之前相互独立,只和引擎进行交互
爬虫中间件和下载中间件只是运行逻辑的位置不同,作用是重复的:如替换UA等
由于scrapy已经帮我们封装了很多的功能,我们只需要对爬虫和管道部分做些补充就好
引擎将定义的类实例化运行
基本流程
安装
sudo apt-get install scrapy 或者:pip install scrapy
1 创建scrapy项目
2 创建爬虫
完善spider
/myspider/myspider/spiders/itcast.py
import scrapy
class ItcastSpider(scrapy.Spider): # 继承scrapy.spider
# 爬虫名字
name = 'itcast'
# 允许爬取的范围
allowed_domains = ['itcast.cn']
# 开始爬取的url地址
start_urls = ['http://www.itcast.cn/channel/teacher.shtml']
# 数据提取的方法,接受下载中间件传过来的response
def parse(self, response):
# scrapy的response对象可以直接进行xpath
names = response.xpath('//div[@class="tea_con"]//li/div/h3/text()')
print(names)
# 获取具体数据文本的方式如下
# 分组
li_list = response.xpath('//div[@class="tea_con"]//li')
for li in li_list:
# 创建一个数据字典
item = {}
# 利用scrapy封装好的xpath选择器定位元素,并通过extract()或extract_first()来获取结果
item['name'] = li.xpath('.//h3/text()').extract_first() # 老师的名字
item['level'] = li.xpath('.//h4/text()').extract_first() # 老师的级别
item['text'] = li.xpath('.//p/text()').extract_first() # 老师的介绍
print(item)
完善spider
class MaitianSpider(scrapy.Spider):
#爬虫名字
name = 'maitian'
#抓取的域名范围,控制范围
allowed_domains = ['maitian.cn']
#
start_urls = ['http://bj.maitian.cn/esfall']
#解析方法
def parse(self, response):
#构建一个容器
item = MiantianItem()
item['title'] = response.xpath('/html/body/section[2]/div[2]/div[2]/ul/li[1]/div[2]/h1/a/text()').extract_first().strip()
item['size_info'] = response.xpath('/html/body/section[2]/div[2]/div[2]/ul/li[1]/div[2]/p/text()').extract()[0].strip()
item['price'] = response.xpath('/html/body/section[2]/div[2]/div[2]/ul/li[1]/div[2]/div/ol/strong/span/text()').extract()[0].strip()
item['location'] = response.xpath('/html/body/section[2]/div[2]/div[2]/ul/li[1]/div[2]/dl/dd/mark[1]/text()').extract()[0]
# print(response.body.decode())
print(item)
#将item返回给engine,engine将数据传给管道
return item
3 运行爬虫
4 解析数据
class MiantianItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
title=scrapy.Field()
size_info=scrapy.Field()
price=scrapy.Field()
location=scrapy.Field()
5 数据存储 管道
修改pipelines.py文件,完善process_item函数
import json
class ItcastPipeline(object):
# 爬虫文件中提取数据的方法每yield一次item,就会运行一次
def __init__(self):
self.file.open('xxx.json','w')
def process_item(self, item, spider):
#将处理后的数据写入文件
str_data = jaon.dumps(item) + ',\n'
#当写入文件出现乱码时,可以在dumps后加入encode_ascii=False参数
self.file.write(str_data)
管道处理完item后必须将item对象返回给引擎。(当定义多个管道类的时候,将item返回,引擎自动下发给下一个管道类。)
return item
def __del__(self):
self.file.close()
mongo
class MongoPipeline(object):
def open_spider(self, spider):
# spider中记录了爬虫名,可以根据爬虫名决定是否使用管道
self.cli = MongoClient("127.0.0.1",27017)
self.col = self.cli.python19.qq
# process_item方法负责定义对数据的处理
def process_item(self, item, spider):
# 将item对象转换成Python字典
item = dict(item)
self.col.insert(item)
# 管道处理完item之后必须将item对象返回给引擎
return item
def close_spider(self, spider):
self.cli.close()
在settings.py设置开启pipeline
ITEM_PIPELINES = { 'myspider.pipelines.ItcastPipeline': 400, 优先级,根据权重值决定各个管道的先后顺序 }
优先级范围从0-1000 优先级越小,优先执行
spider类,item类,中间类,都是写成的类,他们在引擎中被调用,生成实例进行爬取数据
其他操作
自定义中间件
class UaMiddlewares(object):
def __int__(self):
self.user_agent_list=[
'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36',
'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3626.96 Safari/537.36' ,
。。。。。。。。
]
def process_request(self,request,spider):
user_random_agent=random.choice(self.user_agent_list)
request.headers['User-Agent']=user_random_agent
设置header属性
def process_request(self,request,spider):
proxy='http://188.188.188.188:8080'
request.mete['proxy']=proxy
设置meta属性
def process_request(self,request,spider):
proxy='http://188.188.188.188:8080'
request.mete['proxy']=proxy
模拟登录
class ItloginSpider(scrapy.Spider):
name = 'itlogin'
allowed_domains = ['boxuegu.com']
#登页面 找齐需要的参数
start_urls = ['http://tlias-stu.boxuegu.com/#/login']
def parse(self, response):
#代码登陆
formdata={
'loginName':'A180803802',
'password':'yy245088'
}
login_url = 'http://tlias-stu.boxuegu.com/user/login'
#发送post请求
#方法1
# yield scrapy.FormRequest(
# login_url,
# formdata=formdata,
# callback=self.parser_login
# )
#方法2
yield scrapy.Request(login_url,body=json.dumps(formdata),method='POST',callback=self.parser_login)
def parser_login(self,response):
#解析获取的cookie,让scrapy携带着有效的cookie继续请求目标url
target_url='http://tlias-stu.boxuegu.com/#/setinfo'
yield scrapy.Request(target_url,callback=self.parser_info)
def parser_info(self,response):
with open('1.html','wb') as f:
f.write(response.body)
scrapy构造并发送请求
构造Request对象,发送请求
class TencentItem(scrapy.Item):
name = scrapy.Field() # 招聘标题
address = scrapy.Field() # 工作地址
time = scrapy.Field() # 发布时间
job_content = scrapy.Field() # 工作职责
crawlspider
子主题
class LxmlLinkExtractor(FilteringLinkExtractor):
def __init__(self, allow=(), deny=(), allow_domains=(), deny_domains=(), restrict_xpaths=(),
tags=('a', 'area'), attrs=('href',), canonicalize=False,
unique=True, process_value=None, deny_extensions=None, restrict_css=(),
strip=True):
tags, attrs = set(arg_to_iter(tags)), set(arg_to_iter(attrs))
tag_func = lambda x: x in tags
attr_func = lambda x: x in attrs
lx = LxmlParserLinkExtractor(
tag=tag_func,
attr=attr_func,
unique=unique,
process=process_value,
strip=strip,
canonicalized=canonicalize
)
super(LxmlLinkExtractor, self).__init__(lx, allow=allow, deny=deny,
allow_domains=allow_domains, deny_domains=deny_domains,
restrict_xpaths=restrict_xpaths, restrict_css=restrict_css,
canonicalize=canonicalize, deny_extensions=deny_extensions)
def extract_links(self, response):
base_url = get_base_url(response)
if self.restrict_xpaths:
docs = [subdoc
for x in self.restrict_xpaths
for subdoc in response.xpath(x)]
else:
docs = [response.selector]
all_links = []
for doc in docs:
links = self._extract_links(doc, response.url, response.encoding, base_url)
all_links.extend(self._process_links(links))
return unique_list(all_links)
获取所有翻页url的实现
class ErshouSpider(CrawlSpider):
name = 'ershou'
allowed_domains = ['chouti.com']
start_urls = ['https://dig.chouti.com/']
rules = ( #找到翻页的href的正则进行匹配
Rule(LinkExtractor(allow='/all/hot/recent'), callback='parse_item', follow=True),
)
def parse_item(self, response):
print(response.url)
解析方法
def parse_item(self, response):
item = {}
#item['domain_id'] = response.xpath('//input[@id="sid"]/@value').get()
#item['name'] = response.xpath('//div[@id="name"]').get()
#item['description'] = response.xpath('//div[@id="description"]').get()
return item
scapy_redis 分布式
scrapy_splash
import scrapy
from scrapy_splash import SplashRequest # 使用scrapy_splash包提供的request对象
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())
SPLASH_URL = 'http://127.0.0.1:8050'
DOWNLOADER_MIDDLEWARES = {
'scrapy_splash.SplashCookiesMiddleware': 723,
'scrapy_splash.SplashMiddleware': 725,
'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware': 810,
}
DUPEFILTER_CLASS = 'scrapy_splash.SplashAwareDupeFilter'
HTTPCACHE_STORAGE = 'scrapy_splash.SplashAwareFSCacheStorage'
配置
scrapy_redis和scrapy_splash配合使用
重写dupefilter去重类
from __future__ import absolute_import
from copy import deepcopy
from scrapy.utils.request import request_fingerprint
from scrapy.utils.url import canonicalize_url
from scrapy_splash.utils import dict_hash
from scrapy_redis.dupefilter import RFPDupeFilter
def splash_request_fingerprint(request, include_headers=None):
""" Request fingerprint which takes 'splash' meta key into account """
fp = request_fingerprint(request, include_headers=include_headers)
if 'splash' not in request.meta:
return fp
splash_options = deepcopy(request.meta['splash'])
args = splash_options.setdefault('args', {})
if 'url' in args:
args['url'] = canonicalize_url(args['url'], keep_fragments=True)
return dict_hash(splash_options, fp)
class SplashAwareDupeFilter(RFPDupeFilter):
"""
DupeFilter that takes 'splash' meta key in account.
It should be used with SplashMiddleware.
"""
def request_fingerprint(self, request):
return splash_request_fingerprint(request)
settings中设置
# 渲染服务的url
SPLASH_URL = 'http://127.0.0.1:8050'
# 下载器中间件
DOWNLOADER_MIDDLEWARES = {
'scrapy_splash.SplashCookiesMiddleware': 723,
'scrapy_splash.SplashMiddleware': 725,
'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware': 810,
}
# 使用Splash的Http缓存
HTTPCACHE_STORAGE = 'scrapy_splash.SplashAwareFSCacheStorage'
# 去重过滤器
# DUPEFILTER_CLASS = 'scrapy_splash.SplashAwareDupeFilter'
# DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter" # 指纹生成以及去重类
DUPEFILTER_CLASS = 'test_splash.spiders.splash_and_redis.SplashAwareDupeFilter' # 混合去重类的位置
SCHEDULER = "scrapy_redis.scheduler.Scheduler" # 调度器类
SCHEDULER_PERSIST = True # 持久化请求队列和指纹集合, scrapy_redis和scrapy_splash混用使用splash的DupeFilter!
ITEM_PIPELINES = {'scrapy_redis.pipelines.RedisPipeline': 400} # 数据存入redis的管道
REDIS_URL = "redis://127.0.0.1:6379" # redis的url
代码中使用
from scrapy_redis.spiders import RedisSpider
from scrapy_splash import SplashRequest
class SplashAndRedisSpider(RedisSpider):
name = 'splash_and_redis'
allowed_domains = ['baidu.com']
# start_urls = ['https://www.baidu.com/s?wd=13161933309']
redis_key = 'splash_and_redis'
# lpush splash_and_redis 'https://www.baidu.com'
# 分布式的起始的url不能使用splash服务!
# 需要重写dupefilter去重类!
def parse(self, response):
yield SplashRequest('https://www.baidu.com/s?wd=13161933309',
callback=self.parse_splash,
args={'wait': 10}, # 最大超时时间,单位:秒
endpoint='render.html') # 使用splash服务的固定参数
def parse_splash(self, response):
with open('splash_and_redis.html', 'w') as f:
f.write(response.body.decode())
京东体会
1 爬出来大分类底下的小分类的url很多事相对路径,需要在解析方法中使用response.urljoin(子类的url)来进行拼接成一个完整的url
item['link']=response.urljoin(book.xpath("./div[1]/a/@href").extract_first())
2 假如第二个解析方法要使用第一个解析方法的数据,则在第一个解析方法 中给引擎返回请求的时候添加meta参数,是一个字典,达到数据传输的效果
yield scrapy.Request(url,callback=第二个解析方法,meta={'任意的键名':data})
在第二个解析方法中用response.meta['键名']取出传送的data
3在爬取的时候现在element中找,此时的数据时经过浏览器渲染好的,
假如在element中找不到东西了,爬取出现None的情况,就去源码中查找,例如作者名,图片的src在element中跟在源码中是不一样的
item['cover']=book.xpath("./div[1]/a/img/@src|./div[1]/a/img/@data-lazy-img").extract_first()
4 有些数据在源码中都没有就很有可能是通过JS访问响应的接口来获取的,例如这里的价格就是通过访问后端固定的端口获得的,这里就需要用到post请求
5 在访问价格接口的时候,由于在爬虫创建的时候对域做了约束,将会访问不到,这时候需要对allow_domain做添加
6 做爬虫,去大部分很好取,但是涉及到一丁点不同,大的规则就会失效,就需要对规则进行补充,
查找缺失的地方,从小到大一步一步查,直到找到所缺的东西
7 一般在测试的时候在大的节点列表选择时进行切片处理下,少发点请求。
8 对于response.body 得到的是bytes数据,需要对其进行decode才能进行json.loads(),必须是个字符串
9 在编写管道的时候, 主要作用域process_item函数
import json
from pymongo import MongoClient
from scrapy.exporters import JsonItemExporter
class JdbookPipeline(object):
def __init__(self):
self.file=open('1111.json', 'w')
def process_item(self, item, spider):
# 将处理后的数据写入文件
str_data = json.dumps(dict(item),ensure_ascii=False) + ',\n'
# 当写入文件出现乱码时,可以在dumps后加入ensure_ascii=False参数
self.file.write(str_data)
# 管道处理完item后必须将item对象返回给引擎。(当定义多个管道类的时候,将item返回,引擎自动下发给下一个管道类。)
return item
def __del__(self):
self.file.close()
class ItcastMongoPipeline(object):
def open_spider(self, spider): # 在爬虫开启的时候仅执行一次
# 也可以使用isinstanc函数来区分爬虫类:
con = MongoClient(host='127.0.0.1', port=27017) # 实例化mongoclient
self.collection = con.JD.jd # 创建数据库名为itcast,集合名为teachers的集合操作对象
def process_item(self, item, spider):
self.collection.insert(dict(item))
# 此时item对象必须是一个字典,再插入
# 如果此时item是BaseItem则需要先转换为字典:dict(BaseItem)
# 不return的情况下,另一个权重较低的pipeline将不会获得item
return item
10 在改分布式爬虫的时候,allow_domains可以留的,也可以吧他放在def __inti__中