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
image.png

/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 解析数据

image.png

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 数据存储 管道

image.png

修改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

cookie

模拟登录 


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)

meta

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
image.png

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__中

posted @ 2019-09-07 17:40  π=3.1415926  阅读(290)  评论(0编辑  收藏  举报