scrapy框架的使用

scrapy简介

Scrapy 使用了 Twisted异步网络库来处理网络通讯。整体架构大致如下

Scrapy主要包括了以下组件:

  • 引擎(Scrapy)
    用来处理整个系统的数据流处理, 触发事务(框架核心)
  • 调度器(Scheduler)
    用来接受引擎发过来的请求, 压入队列中, 并在引擎再次请求的时候返回. 可以想像成一个URL(抓取网页的网址或者说是链接)的优先队列, 由它来决定下一个要抓取的网址是什么, 同时去除重复的网址
  • 下载器(Downloader)
    用于下载网页内容, 并将网页内容返回给蜘蛛(Scrapy下载器是建立在twisted这个高效的异步模型上的)
  • 爬虫(Spiders)
    爬虫是主要干活的, 用于从特定的网页中提取自己需要的信息, 即所谓的实体(Item)。用户也可以从中提取出链接,让Scrapy继续抓取下一个页面
  • 项目管道(Pipeline)
    负责处理爬虫从网页中抽取的实体,主要的功能是持久化实体、验证实体的有效性、清除不需要的信息。当页面被爬虫解析后,将被发送到项目管道,并经过几个特定的次序处理数据。
  • 下载器中间件(Downloader Middlewares)
    位于Scrapy引擎和下载器之间的框架,主要是处理Scrapy引擎与下载器之间的请求及响应。
  • 爬虫中间件(Spider Middlewares)
    介于Scrapy引擎和爬虫之间的框架,主要工作是处理蜘蛛的响应输入和请求输出。
  • 调度中间件(Scheduler Middewares)
    介于Scrapy引擎和调度之间的中间件,从Scrapy引擎发送到调度的请求和响应。

Scrapy运行流程大概如下:

  1. 引擎从调度器中取出一个链接(URL)用于接下来的抓取
  2. 引擎把URL封装成一个请求(Request)传给下载器
  3. 下载器把资源下载下来,并封装成应答包(Response)
  4. 爬虫解析Response
  5. 解析出实体(Item),则交给实体管道进行进一步的处理
  6. 解析出的是链接(URL),则把URL交给调度器等待抓取

安装

Linux下的安装(包括mac)

  pip install scrapy

Windows下的安装

  a. 下载twisted 
    http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
  b. 安装wheel
    pip3 install wheel
  c. 安装twisted
    进入下载目录,执行 pip3 install Twisted‑18.7.0‑cp36‑cp36m‑win_amd64.whl
  d. 安装pywin32
    pip3 install pywin32
  e. 安装scrapy
    pip3 install scrapy
 

基本命令

 

1. scrapy startproject 项目名称

 

   - 在当前目录中创建一个项目文件(类似于Django)

 

 

 

2. scrapy genspider [-t template] <name> <domain>

 

   - 创建爬虫应用

 

   如:

 

      scrapy gensipider -t basic oldboy oldboy.com

 

      scrapy gensipider -t xmlfeed autohome autohome.com.cn

 

     或者简单直接   scrapy gensipider app名  要爬取的域名

 

   PS:

 

      查看所有命令:scrapy gensipider -l

 

      查看模板命令:scrapy gensipider -d 模板名称

 

 

 

3. scrapy list

 

   - 展示爬虫应用列表

 

 

 

4. scrapy crawl 爬虫应用名称

 

   - 运行单独爬虫应用

 

备注:scrapy crawl 应用名称  表示以日志的形式运行爬虫应用,可以在后面加 --nolog  取消日志    scrapy crawl 名称  --nolog

项目结构以及爬虫应用简介

1
2
3
4
5
6
7
8
9
10
11
12
project_name/
   scrapy.cfg
   project_name/
       __init__.py
       items.py
       middlewares.py
 
       pipelines.py
       settings.py
       spiders/
           __init__.py
           爬虫1.py
           爬虫2.py
           爬虫3.py

文件说明:

  • scrapy.cfg  项目的主配置信息。(真正爬虫相关的配置信息在settings.py文件中)
  • items.py    设置数据存储模板,用于结构化数据,如:Django的Model
  • pipelines    数据处理行为,如:一般结构化的数据持久化
  • settings.py 配置文件,如:递归的层数、并发数,延迟下载等
  • spiders      爬虫目录,如:创建文件,编写爬虫规则

注意:一般创建爬虫文件时,以网站域名命名

windows系统编码错误时:
解决方法:
import sys,io
sys.stdout=io.TextIOWrapper(sys.stdout.buffer,encoding='gb18030')
import scrapy

class ChoutiSpider(scrapy.Spider):
    name = 'chouti'
    allowed_domains = ['chouti.com']
    start_urls = ['https://dig.chouti.com/']

    def parse(self, response):
        pass
爬虫.py
 
备注:可以在settings.py中配置user-agent

在爬取数据时,可以选择是否往.../robots.txt/发送验证,是否允许爬取,一般设置为False

使用scrapy解析文本内容时,可以使用每个应用中的response.xpath(xxx) 进行数据的解析。

print(response.xpath(...))  得到的是一个Selector对象。selector对象可以继续xpath进行数据的解析。
备注:xpath使用方法:
  1.//+标签  表示从全局的子子孙孙中查找标签    
  2./+标签   表示从子代中查找标签
  3.查找带有xxx属性的标签:   标签+[@标签属性="值"]   
  4.查找标签的某个属性:  /标签/@属性  
  5.从当前标签中查找时:.//+标签      
response = HtmlResponse(url='http://example.com', body=html,encoding='utf-8')
hxs = HtmlXPathSelector(response)
print(hxs)   # selector对象
hxs = Selector(response=response).xpath('//a')
print(hxs)    #查找所有的a标签
hxs = Selector(response=response).xpath('//a[2]')
print(hxs)    #查找某一个具体的a标签    取第三个a标签
hxs = Selector(response=response).xpath('//a[@id]')
print(hxs)    #查找所有含有id属性的a标签
hxs = Selector(response=response).xpath('//a[@id="i1"]')
print(hxs)    # 查找含有id=“i1”的a标签
# hxs = Selector(response=response).xpath('//a[@href="link.html"][@id="i1"]')
# print(hxs)   # 查找含有href=‘xxx’并且id=‘xxx’的a标签
# hxs = Selector(response=response).xpath('//a[contains(@href, "link")]')
# print(hxs)   # 查找 href属性值中包含有‘link’的a标签
# hxs = Selector(response=response).xpath('//a[starts-with(@href, "link")]')
# print(hxs)   # 查找 href属性值以‘link’开始的a标签
# hxs = Selector(response=response).xpath('//a[re:test(@id, "i\d+")]')
# print(hxs)   # 正则匹配的用法   匹配id属性的值为数字的a标签
# hxs = Selector(response=response).xpath('//a[re:test(@id, "i\d+")]/text()').extract()
# print(hxs)    # 匹配id属性的值为数字的a标签的文本内容
# hxs = Selector(response=response).xpath('//a[re:test(@id, "i\d+")]/@href').extract()
# print(hxs)    #匹配id属性的值为数字的a标签的href属性值
# hxs = Selector(response=response).xpath('/html/body/ul/li/a/@href').extract()
# print(hxs)
# hxs = Selector(response=response).xpath('//body/ul/li/a/@href').extract_first()
# print(hxs)
 
# ul_list = Selector(response=response).xpath('//body/ul/li')
# for item in ul_list:
#     v = item.xpath('./a/span')
#     # 或
#     # v = item.xpath('a/span')
#     # 或
#     # v = item.xpath('*/a/span')
#     print(v)

 

备注:xpath中支持正则的使用:    用法  标签+[re:test(@属性值,"正则表达式")]
  获取标签的文本内容:   /text()     
  获取第一个值需要  selector_obj.extract_first()    获取所有的值  selector_obj.extract()  值在一个list中

 

scrapy的持久化

scrapy的持久化过程分为四个部分

  首先,items定义传输的格式,其次,在爬虫应用中yield这个item对象,pipeline收到yield的item对象,进行持久化操作,这个过程中,settings中要进行相应的配置

items.py

# 规范持久化的格式
import scrapy


class MyspiderItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    url=scrapy.Field()

爬虫应用

import scrapy
from  myspider.items import MyspiderItem


class ChoutiSpider(scrapy.Spider):
    name = 'chouti'
    allowed_domains = ['chouti.com']
    start_urls = ['https://dig.chouti.com/']

    def parse(self, response):
        # print(response.text)
        a_list = response.xpath('//div[@id="content-list"]//div[@class="part1"]/a[@class="show-content color-chag"]/@href').extract()
        for url in a_list:
            yield MyspiderItem(url=url)

pipelines.py

class MyspiderPipeline(object):

    def __init__(self,file_path):
        self.f = None
        self.file_path = file_path

    @classmethod
    def from_crawler(cls,crawler):
        '''
        执行pipeline类时,会先去类中找from_crawler的方法,
        如果有,则先执行此方法,并且返回一个当前类的对象,
        如果没有,则直接执行初始化方法
        :param crawler:
        :return:
        '''
        # 可以进行一些初始化之前的处理,比如:文件的路径配置到settings文件中,方便后期的更改。
        file_path = crawler.settings.get('PACHONG_FILE_PATH')
        return cls(file_path)

    def open_spider(self,spider):
        '''
        爬虫开始时被调用
        :param spider:
        :return:
        '''
        self.f = open(self.file_path,'w',encoding='utf8')

    def process_item(self, item, spider):
        '''
        执行持久化的逻辑操作
        :param item: 爬虫yield过来的item对象  (一个字典)
        :param spider:  爬虫对象
        :return:
        '''
        self.f.write(item['url']+'\n')
        self.f.flush()   #将写入到内存的文件强刷到文件中,防止夯住,不使用此方法会夯住
        return item

    def close_spider(self,spider):
        '''
        爬虫结束时调用
        :param spider: 
        :return: 
        '''
        self.f.close()

备注:执行pipeline时,会先找from_crawler方法,这个方法中,我们可以设置一些settings文件中的配置,通过crawler.settings得到一个settings对象(配置文件对象) <scrapy.settings.Settings object at 0x000002525581F908>

  执行pipeline中的process_item() 方法进行数据的持久化处理时,如果有多个pipeline(比如:将数据分别写入文件和数据库)时,先执行的pipeline(按照配置文件中数值的大小顺序执行),必须返回一个item对象,否则,后续的pipeline执行时,接收的item为None,无法进行数据的持久化操作,如果只是单纯的对某些数据进行一个持久化的处理,可以通过抛出异常,来阻止当前item对象后续的pipeline执行。抛出异常为:from scrapy.exceptions import DropItem       直接  raise DropItem()

  return不返回item对象与抛异常的区别:无返回值或者返回值为None时,后续的pipeline会执行,只是,此时的item为None,而抛出异常,会跳过当前对象后续的pipeline,执行下一个item对象。

 

settings.py

ITEM_PIPELINES = {  
  'myspider.pipelines.MyspiderPipeline': 300,
  'xxxxx.pipelines.FilePipeline': 400, 
}
# 每行后面的整型值,确定了他们运行的顺序,item按数字从低到高的顺序,通过pipeline,通常将这些数字定义在0-1000范围内。

备注:数值小的先执行。

 获取所有页面

import scrapy
from  myspider.items import MyspiderItem
from scrapy.http import Request

class ChoutiSpider(scrapy.Spider):
    name = 'chouti'
    allowed_domains = ['chouti.com']
    start_urls = ['https://dig.chouti.com/']

    def parse(self, response):
        a_list = response.xpath('//div[@id="content-list"]//div[@class="part1"]/a[@class="show-content color-chag"]/@href').extract()
        for url in a_list:
            yield MyspiderItem(url=url)

        # 获取分页的url
        url_list = response.xpath('//div[@id="dig_lcpage"]//a/@href').extract()
        for url in url_list:
            url = 'https://dig.chouti.com%s'%url
            yield Request(url=url,callback=self.parse)

备注:通过yield 每一个request对象,将所有的页面url添加到调度器中。

  scrapy框架会默认的将所有的结果进行去重操作。如果不去重,可以在request参数中,设置  dont_filter=True

 注意:settings.py中设置DEPTH_LIMIT = 1来指定“递归”的层数   ,这里的层数不是页码数
 
 在生成的每一个爬虫应用中,会有一个起始url,start_urls = ['https://dig.chouti.com/'],这个起始url执行完后会被parse回调函数接收响应结果。那我们如何修改这个回调函数呢?
  其实,在每一个爬虫应用继承的父类中,会执行一个方法  start_requests ,这个方法,会将起始的url生成一个request对象,传给调度器。
class Spider(object_ref):
def start_requests(self): cls = self.__class__ if method_is_overridden(cls, Spider, 'make_requests_from_url'): warnings.warn( "Spider.make_requests_from_url method is deprecated; it " "won't be called in future Scrapy releases. Please " "override Spider.start_requests method instead (see %s.%s)." % ( cls.__module__, cls.__name__ ), ) for url in self.start_urls: yield self.make_requests_from_url(url) else: for url in self.start_urls: yield Request(url, dont_filter=True)

 备注:在执行爬虫应用时,会先执行start_requests方法,所以我们可以重写此方法自定制。

获取响应数据中的cookie

返回的response中,无法通过  .cookies 获取cookie,只能通过从响应头中获取,但是获取的结果还得需要解析。

{b'Server': [b'Tengine'], b'Content-Type': [b'text/html; charset=UTF-8'], b'Date': [
b'Fri, 20 Jul 2018 13:43:42 GMT'], b'Cache-Control': [b'private'], b'Content-Language': [b'en'],
b'Set-Cookie': [b'gpsd=5b05bcae8c6f4a273a53addfc8bbff22; domain=chouti.com; path=/; expires=Sun,
19-Aug-2018 13:43:42 GMT
', b'JSESSIONID=aaadbrXmU-Jh2_kvbaysw; path=/'], b'Vary': [b'Accept-Encoding'],
b'Via': [b'cache15.l2nu29-1[69,0], kunlun9.cn125[73,0]'], b'Timing-Allow-Origin': [b'*'],
b'Eagleid': [b'6a78b50915320942226762320e']}

所以,要通过scrapy封装的方法,将cookie解析出来

import scrapy
from scrapy.http.cookies import CookieJar

class ChoutiSpider(scrapy.Spider):
    name = 'chouti'
    allowed_domains = ['chouti.com']
    start_urls = ['https://dig.chouti.com/']
    cookie_dict = {}
    def parse(self, response):

        cookie_jar = CookieJar()
        cookie_jar.extract_cookies(response,response.request)   
        for k, v in cookie_jar._cookies.items():
            for i, j in v.items():
                for m, n in j.items():
                    self.cookie_dict[m] = n.value
        print(self.cookie_dict)

备注:CookieJar中封装的内容特别丰富,print(cookie_jar._cookies)   包含很多

{'.chouti.com': {'/': {'gpsd': Cookie(version=0, name='gpsd', value='fcb9b9da7aaede0176d2a88cde8b6adb',
port=None, port_specified=False, domain='.chouti.com', domain_specified=True, domain_initial_dot=False,
path='/', path_specified=True, secure=False, expires=1534688487, discard=False, comment=None,
comment_url=None, rest={}, rfc2109=False)}}, 'dig.chouti.com': {'/': {'JSESSIONID':
Cookie(version=0, name='JSESSIONID', value='aaa4GWMivXwJf6ygMaysw', port=None, port_specified=False,
domain='dig.chouti.com', domain_specified=False, domain_initial_dot=False, path='/',
path_specified=True, secure=False, expires=None, discard=True, comment=None, comment_url=None, rest={},
rfc2109=False)}}}

自动登录抽屉并点赞和取消赞代码示例

import scrapy
from scrapy.http.response.html import HtmlResponse
# import sys,os,io
# sys.stdout=io.TextIOWrapper(sys.stdout.buffer,encoding='gb18030')
from ..items import XzxItem
from scrapy.http import Request
from scrapy.http.cookies import CookieJar

class ChoutiSpider(scrapy.Spider):
    name = 'chouti'
    allowed_domains = ['chouti.com']
    start_urls = ['https://dig.chouti.com/r/ask/hot/1']
    cookie_dict = {}
    def start_requests(self):
        for url in self.start_urls:
            yield Request(url=url,callback=self.parse)

    def parse(self, response):
        # 1. 去第一次访问页面中获取cookie
        # print(response.headers['Set-Cookie'],type(response.headers['Set-Cookie']))
        cookie_jar = CookieJar() #
        cookie_jar.extract_cookies(response, response.request) # cookie_jar中包含了cookie


        for k, v in cookie_jar._cookies.items():
            for i, j in v.items():
                for m, n in j.items():
                    self.cookie_dict[m] = n.value
        # 2. 向https://dig.chouti.com/login发送POST请求
        yield Request(
            url='https://dig.chouti.com/login',
            method='POST',
            body="phone=8615901492719&password=qwer1234&oneMonth=1",
            cookies=self.cookie_dict,
            headers={
                'user-agent':'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36',
                'content-type':'application/x-www-form-urlencoded; charset=UTF-8',
            },
            callback=self.check_login
        )

    def check_login(self,response):
        print(response.text)
        yield Request(url='https://dig.chouti.com/',callback=self.index)

    def index(self,response):

        news_id_list = response.xpath('//div[@id="content-list"]//div[@class="part2"]/@share-linkid').extract()
        for news_id in news_id_list:
            #
            """
            news_url = "https://dig.chouti.com/link/vote?linksId=%s" %(news_id,)
            yield Request(
                url=news_url,
                method='POST',
                cookies=self.cookie_dict,
                callback=self.output
            )
            """
            # 取消赞
            news_url = "https://dig.chouti.com/vote/cancel/vote.do"
            yield Request(
                url=news_url,
                method='POST',
                body="linksId=%s" %(news_id,),
                cookies=self.cookie_dict,
                headers={
                    'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
                },
                callback=self.output
            )

        # 2.1 获取页面
        page_list = response.xpath('//*[@id="dig_lcpage"]//a/@href').extract()
        for url in page_list:
            url = "https://dig.chouti.com" + url
            yield Request(url=url,callback=self.index)

    def output(self,response):
        print(response.text)
View Code

备注:爬取过程中的坑:请求头中,一定要携带content-type参数。请求过程中的url不能重复,尤其是和起始url。

我们可以使用urllib中的urlencode帮我们把数据转化为formdata格式的
from urllib.parse import urlencode

ret = {'name':'xxx','age':18}

print(urlencode(ret))

 

 
posted @ 2018-08-08 19:23  一抹浅笑  阅读(13801)  评论(0编辑  收藏  举报