首页 > 技术文章 > Scrapy爬虫框架学习

lisenlin 2019-04-06 09:43 原文

一、Scrapy框架简介

1. 下载页面
2. 解析
3. 并发
4. 深度

 

二、安装

linux下安装    
    pip3 install scrapy
    
windows下安装
    a.pip3 install wheel
    b.下载twisted和pywin32 http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
    c.进入下载目录
         执行pip3 install Twisted-18.7.0-cp36-cp36m-win_amd64.whl     #cp36为适合python3.6
         执行pip3 install  pywin32-224-cp36-cp36m-win_amd64.whl
  d.pip3 install scrapy
    e.下载并安装pywin32:https://sourceforge.net/projects/pywin32/files/  #找到适合本机python版本的64位

 

 

三、Scrapy整体架构图

  3.1 Scrapy使用Twisted异步网络库来处理网络通讯

 

  3.2 各个主要文件说明

  spiders(蜘蛛)文件夹:如ip138.com

name    #不能省略,最好不要修改
starts_urls  #起始url
allowed_domains    #爬取允许域名列表,起始url不受影响。网页中的外链受此限制

 

 

 

四、使用

  4.1 基本使用

1. 指定初始url
2. 解析器响应内容
    - 给调度器
    - 给item:pipeline;用于做格式化;持久化

基本步骤:
a:scrapy startproject 项目名   #创建项目
b:进入项目目录
c:scrapy genspider baidu www.baidu.com  #创建start_url
d:打开项目名\spiders\baidu.py进行编辑
e:scrapy crawl baidu    #执行,加--nolog可以不显示日志,如果没有内容显示,可能此IP已经有防爬机制,可换个不知名ip试试
#此处的baidu不是项目名,而是spiders文件夹下的baidu.py

*f:scrapy shell http://www.baidu.com #下载url里的内容,进入调试模式,对于捕捉过滤字段非常方便

   

  4.2 scrapy.loader.ItemLoader的使用

    a.Itemloader介绍

优点:避免重复代码,提高代码可读性
    (解决在爬虫文件中代码结构杂乱,无序,可读性差的缺点)

 

    b.Itemloader简单使用

#ke.py爬虫文件
from scrapy.loader import ItemLoader
from .. import items     #导入items.py文件里的类

class KeSpider(scrapy.Spider):
    name = 'ke'
    allowed_domains = ['ke.com']
    start_urls = ['https://sz.ke.com/xiaoqu/']

    def parse(self, response):
        # 通过变量item_loader加载item
        item_loader = ItemLoader(item=items.BeiKeItem(), response=response)
        item_loader.add_css()  #使用css选择器
        item_loader.add_xpath('name', '//div[@class="title"]/h1/text()')  # 将response用xpath选择器指定的过滤字段,得到的结果赋值给items.py文件里BeiKeItem类里的name变量
        item_loader.add_value('accessUrl', response.url)  # 将response.url的值赋值给items.py文件里BeiKeItem类里的accessUrl变量

        item_obj = item_loader.load_item()      #将item_loader里的值装入Item
        yield item_obj

 

 

 

  4.3 scrapy.loader.processors.MapCompose使用

    a.MapCompose()介绍

MapCompose()
#传入函数的列表的每一个元素都会经过第一个函数,
#得到值在经过第二个函数,如果有返回值为None的,则抛弃,
#最后返回一个列表

   

    b.简单使用

#items.py文件
class BeiKeItem(scrapy.Item):
    name = scrapy.Field(input_processor=MapCompose(addName)) #input_processor输入时进行预处理,MapCompose为加载函数处理,每一个元素后面都加上"-伍木"
    name = scrapy.Field(input_processor=MapCompose(lambda x:x+"-伍木")) #input_processor输入时进行预处理,MapCompose为加载lambda处理,每一个元素后面都加上"-伍木"
    name = scrapy.Field(input_processor=MapCompose(lambda x:x+"-伍木1",addName)) #MapCompose第一个参数传入lambda,第二个参数传入函数,每一个元素先经过第一个lambda,结果在一一经过第二个函数

 

  4.4 scrapy.loader.processors.TakeFirst使用

              a.TakeFirst()介绍

只取列表里对象元素的第一个值

    b.简单使用

#items.py文件
class BeiKeItem(scrapy.Item):
    name = scrapy.Field(out_processor = TakeFirst())    #tekeFirst()函数只取列表第一个元素,即便空列表也不报错

   c.让每一个Item对象输出结果都默认只取第一个元素

笨办法(每个字段输出时都用一次TakeFirst()):
#items.py
from scrapy.loader.processors import TakeFirst
class BeiKeItem(scrapy.Item):
    name = scrapy.Field(out_processor = TakeFirst())    #tekeFirst()函数只取列表第一个元素,即便空列表也不报错
    avgPrice = scrapy.Field(out_processor = TakeFirst())
    accessUrl = scrapy.Field(out_processor = TakeFirst())
    address = scrapy.Field(out_processor = TakeFirst())

###############################################################################################################

聪明办法(改写ItemLoader),一次改写,无需重复
#items.py
from scrapy.loader.processors import TakeFirst
from scrapy.loader import ItemLoader
class BeiKeItemLoader(ItemLoader):
    #自定义itemloader
    default_output_processor = TakeFirst()

#爬虫文件ke.py 
from scrapy.loader import ItemLoader

class KeSpider(scrapy.Spider):
    name = 'ke'
    allowed_domains = ['ke.com']
    start_urls = ['https://sz.ke.com/xiaoqu/']

    def parse(self, response):
         item_loader = BeiKeItemLoader(item=items.BeiKeItem(), response=response) #将ItemLoader换成重写的BeiKeItemLoader
item_loader.add_xpath('name', '//div[@class="title"]/h1/text()')  
        item_loader.add_value('accessUrl', response.url)
        item_obj = item_loader.load_item()   
        yield item_obj

  

  4.5 scrapy.loader.processors.Join

    a. Join()介绍

将元素那指定分隔符拼接,()括号内为指定分隔符,如","

    

    b.简单使用

#items.py文件
from scrapy.loader.processors import Join
class BeiKeItem(scrapy.Item):
    tag = scrapy.Field(out_processor = Join(','))   #将输出的元素以“,”分割,返回字符串类型    

 

 

五、筛选器

  5.1  Selector介绍

  在scrapy中,可以使用Selector筛选器,代替BeautifulSoup

  在scrapy项目里的spiders文件里的baidu.py爬虫文件编辑,导入Selecotr模块

from scrapy.selector import Selector

 

  5.2 Selector(response=response).xpath()基本用法  或者使用response.xpath()

//        #表示子子孙孙,即所有
.//        #当前对象的子孙中
/            #儿子
/div        #儿子中的div标签
//div[@id]  #所有标签含有id的div标签
/div[@id='i1']    #儿子中的div且id ='i1'的标签
obj.extract()        #列表中每一个对象转换成字符串 ==>返回的是列表
obj.extract_first()  #列表中每一各对象转换成字符串==>返回的是列表中第一个元素
//div/text()        #获取所有div标签里的文本==》返回的是列表,元素是对象
//a/@href           #获取所有a标签里的url==》返回的是列表,元素是对象
//a[starts-with(@href,"link")]    #获取所有a标签,并且href属性是以link开头的
//a[re:test(@href,"/sitehome/p/\d+")]/@href   #正则,获取所有a标签属性href符合/sitehome/p/数字的
//div[@id='i1'][@href=''xx]   #[][]且的意思,即所有含有id='i1'且href='xxx'的div标签
//a[contains(@href, "link")]    #所有href字段包含link字符串的a标签

 

 

  5.3 简单示例

  需求:在www.ip138.com网站里,打印如下a标签的文本内容和url地址

 

  在爬虫文件(baidu.py)里的parse类方法里编辑 

# -*- coding: utf-8 -*-
import scrapy
from scrapy.selector import Selector

class BaiduSpider(scrapy.Spider):
    name = 'baidu'
    allowed_domains = ['ip138.com']
    start_urls = ['http://www.ip138.com/']

    def parse(self, response):
        #请求该页面下所有a标签==》列表,每个元素都是对象
        #找到div标签里有class=mod-guide属性下的所有子孙a标签里文本内容
        text_obj_list = Selector(response=response).xpath('//div[@class="module mod-guide"]//a/text()')

        # 找到div标签里有class=mod-guide属性下的所有子孙a标签里url
        url_obj_list = Selector(response=response).xpath('//div[@class="module mod-guide"]//a/@href')

        #将列表中对象转化成列表中字符串
        text_str_list = text_obj_list.extract()
        url_str_list = url_obj_list.extract()

        for a_text,a_url in zip(text_str_list,url_str_list):
            print(a_text,a_url)
代码

 

  如何运行代码?

在cmd窗口,进入项目目录,运行scrapy  crawl  baidu

 

  5.4 获取当前网页中的所有页码实例

  需求:获取博客园里的首页页码url

 

  

  在爬虫文件(ip138.py)里的parse类方法里编辑

# -*- coding: utf-8 -*-
import scrapy
from scrapy.selector import Selector

class Ip138Spider(scrapy.Spider):
    name = 'ip138'
    allowed_domains = ['www.cnblogs.com']
    start_urls = ['https://www.cnblogs.com/']

    urls_set = set()

    def parse(self, response):
        # text_obj_list = Selector(response=response).xpath('//div[@class="module mod-guide"]//a/text()')
        # url_obj_list = Selector(response=response).xpath('//div[@class="module mod-guide"]//a/@href')
        #
        # text_str_list = text_obj_list.extract()
        # url_str_list = url_obj_list.extract()
        #
        # for text,url in zip(text_str_list,url_str_list):
        #     print(text,url)

        #获取当前页里的所有页码的url对象列表
        url_obj_list = Selector(response=response).xpath('//div[@class="pager"]/a/@href')
        ##或者2: starts-with(@属性,'值开头')
        #url_obj_list = Selector(response=response).xpath('//a[starts-with(@href,"/sitehome/p/")]/@href')
        ##或者3:正则表达式固定用法re:test(@属性值,"正则表达式")
        #url_obj_list = Selector(response=response).xpath('//a[re:test(@href,"/sitehome/p/\d+")]/@href')

        #将对象列表转换成字符串列表
        url_str_list = url_obj_list.extract()
        for url in url_str_list:
            print(url)

            #1.通过集合去除重复url
            #2.使用加密的MD5存储url,好处:加密和等长
            url_md5 = self.my_md5(url)
            if url_md5 not in self.urls_set:
                self.urls_set.add(url_md5)
                print(url_md5)
            else:
                print('%s已经存在'%url_md5)

    def my_md5(self,url):
        import hashlib
        obj = hashlib.md5()
        obj.update(bytes(url,encoding='utf-8'))
        return obj.hexdigest()
代码

 

  5.5 获得当前网页里的页码自动请求爬取实例

  需求:自动爬取博客园的所有页码(基于5.4案例的基础)

  

  在爬虫文件(ip138.py)里的parse类方法里编辑

# -*- coding: utf-8 -*-
import scrapy
from scrapy.selector import Selector
from scrapy.http import Request

class Ip138Spider(scrapy.Spider):
    name = 'ip138'
    allowed_domains = ['www.cnblogs.com']
    start_urls = ['https://www.cnblogs.com/']

    urls_set = set()

    def parse(self, response):

        #获取当前页里的所有页码的url对象列表
        url_obj_list = Selector(response=response).xpath('//div[@class="pager"]/a/@href')
        ##或者2: starts-with(@属性,'值开头')
        #url_obj_list = Selector(response=response).xpath('//a[starts-with(@href,"/sitehome/p/")]/@href')
        ##或者3:正则表达式固定用法re:test(@属性值,"正则表达式")
        #url_obj_list = Selector(response=response).xpath('//a[re:test(@href,"/sitehome/p/\d+")]/@href')

        #将对象列表转换成字符串列表
        url_str_list = url_obj_list.extract()
        for url in url_str_list:

            #1.通过集合去除重复url
            if url not in self.urls_set:
                #print(url)
                self.urls_set.add(url)
                #拼接完整的url地址
                full_url = self.start_urls[0] + url
                #将url页码传给调度器去请求,并将下载的结果交给parse方法,yield的作用是将请求放入调度器
                yield Request(url=full_url,callback=self.parse)

            for url in self.urls_set:
                print(url)
代码

 

  在setting.py中结尾新增一行DEPTH_LIMIT=1来指定递归的层次,默认是0,所有层次

   实例中关键点概括

@href    #取属性值
starts-with(@href,"xx")   #属性href的值以xx开始
re:test(@href,"/sitehome/p/\d+")   #正则re:test固定搭配
yield Request(url=full_url,callback=self.parse)   #交给调度器   或者使用 yield response.follow(url=full_url,callback=self.parse),效果一样

 

六、item,pipeline使用

  6.1 需求:将博客园的文章标题和url保存到一个文件a.txt文件里。

     各文件代码如下:

# -*- coding: utf-8 -*-
import scrapy
from scrapy.selector import Selector
from scrapy.http import Request
from .. import items

class Ip138Spider(scrapy.Spider):
    name = 'ip138'
    allowed_domains = ['www.cnblogs.com']
    start_urls = ['https://www.cnblogs.com/']

    urls_set = set()

    def parse(self, response):

        #获取当前页面里的标题和url==>返回字符串
        title_str_list = Selector(response=response).xpath('//a[@class="titlelnk"]/text()').extract()
        href_str_list = Selector(response=response).xpath('//a[@class="titlelnk"]/@href').extract()

        for title_str,href_str in zip(title_str_list,href_str_list):
            #print(title_str,'   ',href_str)
            
            #此处的参数title,和href来自于items.py文件里类Cnblogs里的属性
            item_obj = items.Cnblogs(title=title_str,href=href_str)

            #将item对象传递给pipelines
            yield item_obj



        #获取当前页里的所有页码的url对象列表
        page_obj_list = Selector(response=response).xpath('//div[@class="pager"]/a/@href')
        ##或者2: starts-with(@属性,'值开头')
        #page_obj_list = Selector(response=response).xpath('//a[starts-with(@href,"/sitehome/p/")]/@href')
        ##或者3:正则表达式固定用法re:test(@属性值,"正则表达式")
        #page_obj_list = Selector(response=response).xpath('//a[re:test(@href,"/sitehome/p/\d+")]/@href')

        #将对象列表转换成字符串列表
        page_str_list = page_obj_list.extract()
        for url in page_str_list:

            #1.通过集合去除重复url
            if url not in self.urls_set:
                self.urls_set.add(url)
                print(url)

                #拼接完整的url地址
                full_url = self.start_urls[0] + url
                #将url页码传给调度器去请求,并将下载的结果交给parse方法,yield的作用是将请求放入调度器
                yield Request(url=full_url,callback=self.parse)
ip138.py文件
# -*- coding: utf-8 -*-

# Define here the models for your scraped items
#
# See documentation in:
# https://doc.scrapy.org/en/latest/topics/items.html

import scrapy


class Cnblogs(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    title = scrapy.Field()
    href = scrapy.Field()
items.py文件
# -*- coding: utf-8 -*-

# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: https://doc.scrapy.org/en/latest/topics/item-pipeline.html


class Day0310Pipeline(object):
    def process_item(self, item, spider):
        content = "%s    %s\n"%(item['title'],item['href'])
        f = open('a.json','a')
        f.write(content)
        f.close()
        # return item
pipelines.py文件
ITEM_PIPELINES = {
   'day0310.pipelines.Day0310Pipeline': 300,
}
setting.py文件

 

  6.2 实例中关键点概

#items.py文件中
class Cnblogs(scrapy.Item):     
    title = scrapy.Field()
    href = scrapy.Field()

#ip138.py文件中
from .. import items
item_obj = items.Cnblogs(title=title_str,href=href_str)   #此处的参数title,和href来自于items.py文件里类Cnblogs里的属性

yield item_obj        #将item对象传递给pipelines

#pipelines.py文件中,将数据保存在文本a.txt里
class Day0310Pipeline(object):
    def process_item(self, item, spider):   #item为ip138.py文件里的对象化的数据,spider为来自哪只蜘蛛对象
        content = "%s    %s\n"%(item['title'],item['href'])
        f = open('a.txt','a')
        f.write(content)
        f.close()

#【可选】pipelines.py文件中,将数据保存在csv文件里
import csv
class Day0310Pipeline(object):
    def process_item(self, item, spider):   #item为ip138.py文件里的对象化的数据,spider为来自哪只蜘蛛对象
        with open("a_gbk.csv", "a", encoding="gbk") as f:    #此处encoding指定为gbk,是因此csv在windows中文系统下打开是GBK编码
                                                            #可参考文章https://www.cnblogs.com/lisenlin/p/14241416.html里的“四、常见问题”4.2的解决方法一
            csv_writer = csv.writer(f)
            csv_writer.writerow(item.values())    #将字典里的每个值存放到csv的行里每个单元格


#setting.py文件中注册下pipelines的类

ITEM_PIPELINES = {
   'day0310.pipelines.Day0310Pipeline': 300,
}
数字越小越先进入管道

 

推荐阅读