Scrapy(一)- 基本使用,爬虫实例

Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架。 其可以应用在数据挖掘,信息处理或存储历史数据等一系列的程序中。

其最初是为了页面抓取 (更确切来说, 网络抓取 )所设计的, 也可以应用在获取API所返回的数据(例如 Amazon Associates Web Services ) 或者通用的网络爬虫。Scrapy用途广泛,可以用于数据挖掘、监测和自动化测试。

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

《Scrapy(一)- 基本使用,爬虫实例》

Scrapy主要包括了以下组件(结合上图):

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

 
Scrapy运行流程大概如下(结合上图):

  • 每个 Spiders 都会有一个初始start_urls。(第一次运行)
    引擎第一次运行时把 Spiders 里的start_urls全放到调度器里
  • 然后引擎从调度器中取出一个链接(URL)用于接下来的抓取
  • 引擎把URL封装成一个请求(Request)传给下载器
  • 下载器把资源下载下来,并封装成应答包(Response)
  • 爬虫解析Response
  • 解析出实体(Item),则交给实体管道进行进一步的处理
  • 解析出的是链接(URL),则把URL交给调度器等待抓取

Scrapy 安装

Linux
      pip3 install scrapy
  
Windows
      a. pip3 install wheel
      b. 下载twisted http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
      c. 进入下载目录,执行 pip3 install Twisted‑17.1.0‑cp35‑cp35m‑win_amd64.whl
      d. pip3 install scrapy
      e. 下载并安装pywin32:https://sourceforge.net/projects/pywin32/files/

Scrapy 基本使用

基本命令(cmd):

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
   PS:
      查看所有命令:scrapy gensipider -l
      查看模板命令:scrapy gensipider -d 模板名称
 
3. scrapy list
   - 展示爬虫应用列表
 
4. scrapy crawl 爬虫应用名称
   - 运行单独爬虫应用

   如果想在通过main.py运行,则新建main.py然后
   from scrapy import cmdline
   cmdline.execute("scrapy crawl chouti --nolog".split())

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

project_name/
   scrapy.cfg
   project_name/
       __init__.py
       items.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 爬虫目录,如:创建文件,编写爬虫规则
运行 scrapy genspider zhihu zhihu.com 后生成的.py文件

import scrapy
 
class ZhiHuSpider(scrapy.spiders.Spider):
    name = "zhihu"                            # 爬虫名称 *****
    allowed_domains = ["zhihu.com"]  # 允许的域名
    start_urls = [
        "https://www.zhihu.com/",   # 起始 URL
    ]
 
    def parse(self, response):
        # 访问起始URL并获取结果后的回调函数

# 如果在Windows里遇到编码问题,输入下面代码
import sys,os
sys.stdout=io.TextIOWrapper(sys.stdout.buffer,encoding='gb18030')

xpath语法

nodename    选取此节点的所有子节点。
/   从根节点选取。
//  从匹配选择的当前节点选择文档中的节点,而不考虑它们的位置。
.   选取当前节点。
..  选取当前节点的父节点。
@   选取属性。
如:
//book  选取所有 book 子元素,而不管它们在文档中的位置。
bookstore//book 选择属于 bookstore 元素的后代的所有 book 元素,而不管它们位于 bookstore 之下的什么位置。

/div[@id="i1"] 儿子中id="i1"的div
/div[@id="i1"]/text() 获取当前标签(div)的文本
/div[@id="i1"]/@href  获取当前标签(div)的href属性
obj.extract() 列表中的每一个对象转换成字符串,返回一个列表
obj.extract_first() 列表中的每一个对象转换成字符串,返回列表第一个元素

# a[starts-with(@属性(href), "link"),获取以link开头的
hxs = Selector(response=response).xpath('//a[starts-with(@href, "/all/hot/recent/")]')
# 也可以用正则表达式a[re:test(@href, "...")]
hxs = Selector(response=response).xpath('//a[re:test(@href, "/all/hot/recent/\d+")]')

# contains代表包含link就行
# hxs = Selector(response=response).xpath('//a[contains(@href, "link")]')

低级去重方式

我们通过爬虫获取到的 url 肯定会有重复的,所以要去重,假如我们用集合去重(低级方式):

# 定义静态字段
visited_urls = set()
...
def parse(self, response):
    for url in hxs:
        if url in self.visited_urls:
            print('已经存在', url)
        else:
            print(url)
            self.visited_urls.add(url)

这样就去重了,但还有问题,当我们存储时(无论存在数据库还是内存/缓存中),如果url的长度过大,就会导致内存的浪费,所以我们需要通过md5或者其他加密算法,将url加密,这样就固定了存储url的长度。

首先定义加密方法:

def md5(self, url):
    import hashlib
    obj = hashlib.md5()
    obj.update(bytes(url, enco
    return obj.hexdigest()

然后将上面的改成:

hxs = Selector(response=response).xpath('//a[starts-with(@href, "/all/hot/recent/")]')
for url in hxs:
    md5_url = self.md5(url)
    if md5_url in self.visited_urls:
        print('已经存在', url)
    else:
        self.visited_urls.add(md5_url)
        real_url = "http://dig.chouti.com/%s"%(url)
        print(real_url)

运用 Request 发送请求

获取当前页的所有页码已经完成了,这时需求改了,要获取所有的页码怎么做?
获取所有页,就是从当前页跳转,然后再到跳转的页继续找,重复这个动作,这样就像是递归,而在Scrapy里,为我们提供了一个Request对象。用来请求数据(跳转就是请求新的页码)。

from scrapy.http import Request
    ...
    else:
        self.visited_urls.add(md5_url)
        real_url = "http://dig.chouti.com%s"%(url)
        print(real_url)
        # 将新要访问的url添加到调度器
        # 必须写yield,写了yield引擎才能把Request发给调度器
        yield Request(url=real_url, callback=self.parse)

这样就能找到所有的页码,如果我们不想要这么多,就可以在settings.py里设置递归深度,如

DEPTH_LIMIT = 2
# DEPTH_LIMIT = 0 表示无限制

数据持久化:

Scrapy里数据的持久化是在Pipeline和item里做的。
首先在items.py里,将爬取的东西作为一个对象

class ChoutiItem(scrapy.Item):
    title = scrapy.Field()
    href = scrapy.Field()

然后chouti.py爬的时候存数据:

xs_item_list = Selector(response=response).xpath('//div[@id="content-list"]/div[@class="item"]')
        for item in hxs_item_list:
            title = item.xpath('.//a[@class="show-content color-chag"]/text()').extract_first().strip()
            href = item.xpath('.//a[@class="show-content color-chag"]/@href').extract_first().strip()
            # 存数据
            item = ChoutiItem(title=title, href=href)
            # 通过yield 发送给Pipeline
            yield item

然后再Pipeline里进行持久化:

pieplines.py
class ScrapyLearnPipeline(object):
    # process 处理
    def process_item(self, item, spider):
        # 因为所有爬虫爬的数据都放在pipeline里做持久化,所以可以根据spider.name来做区分
        if spider.name == 'chouti':
            # 从item里取数据通过item['xxx']
            tmp = "%s\n%s\n\n"%(item['title'], item['href'])
            with open('data.json', 'a') as f:
                f.write(tmp)

还需要注意的是,要在settings.py中注册Pipeline:

ITEM_PIPELINES = {
   'scrapy_learn.pipelines.ScrapyLearnPipeline': 300,
}
300是权重,权重大,则先持久化

去重的新方式

  • 自定义类去重
  • 用scrapy自带的类(RFPDupeFilter),他是存在文件中,然后通过文件来判断的。(from scrapy.dupefilters import RFPDupeFilter)

自定义类:

新建duplication.py
class RepeatFilter(object):
    def __init__(self):
        # 自定义的类可以存在内存,缓存,数据库,文件
        self.visited_set = set()

    @classmethod
    def from_settings(cls, settings):
        # 创建RepeatFilter对象 - cls()
        return cls()

    # 检查是否访问过
    def request_seen(self, request):
        if request.url in self.visited_set:
            return True
        self.visited_set.add(request.url)
        return False

    def open(self):  # can return deferred
        # print('open') 开始爬取
        pass

    def close(self, reason):  # can return a deferred
        # print('close') 结束爬取
        pass

    def log(self, request, spider):  # log that a request has been filtered
        # print('log....') 打印日志
        pass

执行顺序是 from_settings -> __init__ -> open -> request_seen -> close
自定义类还需要更改下配置文件:

DUPEFILTER_CLASS = "day96.duplication.RepeatFilter"
// 默认是 DUPEFILTER_CLASS = "scrapy.dupefilters.RFPDupeFilter",即scrapy自带的去重

Pipeline深入分析

Pipeline除了process_item()还有from_crawler,open_spider,close_spider方法。
from_crawler类似自定义去重类中的from_settings,也是一个类方法

class ScrapyLearnPipeline(object):  
    def __init__(self,conn_str):
        """
        初始化数据
        """
        self.conn_str = conn_str

    @classmethod
    def from_crawler(cls, crawler):
        """
        初始化时调用,用于创建pipeline对象,得到配置文件
        还可以进行数据库配置(数据库配置都是在settings.py里进行的)
        """
        conn_str = crawler.settings.get('DB') # 注意配置文件中的变量都要大写
        return cls(conn_str)

    def open_spider(self,spider):
        """
        爬虫开始执行时,调用,如果存储用的是文件/数据库等可以在里面打开文件,这样就实现了打开一次文件,写入完后再关闭文件
        """
        self.conn = open(self.conn_str, 'a')

    def close_spider(self,spider):
        """
        爬虫关闭时,被调用
        """
        self.conn.close()

    def process_item(self, item, spider):
        """
        每当数据需要持久化时,就会被调用
        """
        # if spider.name == 'chouti'
        tpl = "%s\n%s\n\n" %(item['title'],item['href'])
        self.conn.write(tpl)

        # 注意如果有多个方法,一定要return,这样才能交给下一个pipeline处理
        return item

        # 当不需要交个下一个pipeline处理时,请务必这样写(from scrapy.exceptions import DropItem)
        # raise DropItem()

Cookie问题

当爬取的网站需要登录时,需要携带请求头,请求体,Cookie。

登录流程:
访问当前页面,拿到返回的GPSD,然后带着GPSD去登录,然后服务器就会将我们的GPSD授权,授权后GPSD就可以一直用了。具体操作看实例。

实例

自动给抽屉点赞

/spiders/chouti.py

import scrapy
from scrapy.selector import Selector, HtmlXPathSelector
from scrapy.http import Request
from scrapy.http.cookies import CookieJar

class ChoutiSpider(scrapy.Spider):
    name = 'chouti'
    # allowed_domains = ['chouti.com/']
    start_urls = ['http://dig.chouti.com/']
    # 存储cookie
    cookie_dict = None

    def parse(self, response):
        # 由于有start_urls的存在,所以会自动访问一次页面
        # 创建cookie对象,现在里面还什么都没有,只是一个容器
        cookie_obj = CookieJar()

        # extract_cookies()需要两个参数,response和request,而reponse里包含request,这一步得到授权的cookie
        cookie_obj.extract_cookies(response, response.request)

        # cookie_obj._cookies 从cookie对象中拿到cookie
        cookie_dict = cookie_obj._cookies

        yield Request(
            # 发起登录请求
            url="http://dig.chouti.com/login",
            method="POST",
            headers={'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'},
            # 请求体写法:= 和 &
            body="phone=86xxx&password=xxx&oneMonth=1",
            cookies=self.cookie_dict,
            callback=check_login
        )

    def check_login(self, response):
        yield Request(url='http://dig.chouti.com/', callback=self.like)

    # 点赞
    def like(self, response):
        # 拿到赞的id列表
        id_list = Selector(response).xpath('//div[@share-linkid]/@share-linkid').extract()
        for nid in id_list:
            # 每个赞的url
            url - 'http://dig.chouti.com/link/vote?linksId=%s'%(nid)
            yield Request(
                url=url,
                method='POST',
                # 点赞需要携带cookie
                cookies=self.cookie_dict,
                callback=self.success
            )

        # 查找下一页
        page_urls = Selector(response).xpath('//div[@id="dig_lcpage"]//a/@href').extract()
        for page in page_urls:
            url = 'http://dig.chouti.com%s'%(page)
            yield Request(url=url, callback=self.like)

    def success(self, response):
        print(response.text)

具体源码:猛戳这里

    原文作者:Zoulf
    原文地址: https://www.jianshu.com/p/d8984c2a237b
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞