Python初识——Scrapy抓取二次元小姐姐图片

写在前面

最近在学习Python这门语言,禀着实践是最好的学习方法的原则,上来就迫不及待的学习了Scrapy框架,并结合网上的例子进行了实验,不得不说感觉到了Python和Scrapy的强大之处,同时也感觉算是站在Python的门外,窥得厅堂里面的东西,还是有些小激动的。那么本帖算是对往日工作的一个总结,也希望自己能够把总结这个习惯坚持下去,一方面帮助自己,一方面帮助他人。


参考链接

在此还是很感谢网上许许多多大神们的帖子和经验,这里先列出学习过程中参考的链接,希望可以帮助更多的人。

Scrapy tutorial:https://doc.scrapy.org/en/latest/intro/tutorial.html  很有用的官方教程,建议初学者看一下

知乎参考程序:https://www.zhihu.com/question/27621722/answer/269085034 个人感觉还是很有意思的,程序理解起来也不难

Python3语句变为Python2:http://blog.csdn.net/whatday/article/details/54710403 这个博客讲了urllib库在Python2和Python3中的区别,想用Python2的话就把里面对应的语句改一下就可以了(亲测好用)。


Scrapy浅析

首先奉上整个框架的框架图:

Python初识——Scrapy抓取二次元小姐姐图片_第1张图片

可以看到,Scrapy主要包含一下几个方面的组件:

Scrapy Engine(引擎):主要用来处理整个系统的数据流,触发事务。

Scheduler(调度器):主要接收引擎发过来的请求,途中的数据流表示爬虫(spider)将请求给引擎,引擎再给调度器。这个调度器可以看成是一个带有去除重复URL的队列,由他决定下一时刻要去抓取哪个网页,同时去除重复的网址。

Downloader(下载器):由图可以看到,Downloader用于接收Scheduler的请求,之后就开始对请求的网页进行下载,下载后的网页将储存在Response中给spider。

Spider(爬虫):这部分就是我们需要重点对待的部分了,也是程序的主题部分。当spider收到回复之后,就可以使用Scrapy的Selector进行感兴趣内容的抓取,同时还可以生成下次要抓取的页面通过请求的方式给引擎。

ItemPipeline(项目管道):Item是整个抓取过程的实体,也就是我们所需要的资源,个人认为一般就是我们需要的链接,例如图片,视频等的URL;而Pipeline接到这样的实体之后,就可以对实体中的内容进行处理,例如下载图片和音频等工作,这部分也是我们需要编程的部分。

Middlewares(中间件):各种中间件,从图中来看应该是用于数据的流向,具体的功能还没有研究到。


再来大致讲一下整个框架的运行流程:

1. 爬虫将URL以请求(Request)的形式发送给引擎,引擎给予调度器。

2. 引擎从调度器中取出将要抓取的URL(更确切的说是请求),发送给下载器,下载器对该URL进行下载。

3. 下载器将下载的内容以回复(Response)的方式给引擎,引擎转送给爬虫。

4. 爬虫对回复进行解析,将解析出的实体(Item)通过引擎给予管道(Pipeline),将解析出的URL(下一次要抓取的网页)通过引擎给调度器。

5. 管道接收到实体之后对其中的资源进行下载。


Scrapy抓取http://www.acg.fi/

作为一个来自二次元的程序员,这个网站着实让人眼前一亮,毕竟之前只知道B站:D

废话不多说,直接说如何进行该网站的抓取。

首先用浏览器打开要抓取的网页acg.fi,随后我们查看他的源代码,发现这个网站的源代码真心的乱啊,一点儿都不归正:|不过没有关系,我们可以用检查的模式进行代码的浏览,很快就能找到我们想要解析的地方如下图所示

Python初识——Scrapy抓取二次元小姐姐图片_第2张图片

可以看到我们主要要解析的标签为

  • 下的标签,通过总结我们发现
  • 标签下的id属性里都有“menu-item”这样的字段,很方便的就可以区分与其他的
  • 标签,这时候我们就可以编写代码了,这里主要用xpath的方法,程序也很简单,rlt = response.xpath("//li[contains(@id,'menu-item')]//a/@href").extract()就可以提取到上面的七个板块的URL了,但是请注意并不是所有的版块都是有图的,比如上面的信息页和大事记等,因此还要进行列表(list)的切片操作,随后把URL封装成Request的形式发送出去就好了。这里还是强烈建议新手多看看scrapy的tutorial里面关于Selector的讲解,很有受用(链接:https://doc.scrapy.org/en/latest/topics/selectors.html)

    如果这个地方没有了问题,那么解析网页基本上就没啥大问题了,可以看到后面的操作基本上都是这么找标签,然后提取。爬虫部分的代码也就可以贴上来了:D


    spider代码

    #!/usr/bin/env python
    # coding:utf-8
    
    from acg.items import AcgItem;
    from acg import settings;
    import os;
    import scrapy;
    import urllib.request;
    import urllib.parse;
    
    
    class acgspiders(scrapy.Spider):
        ''' 爬取一个网站 '''
        name = 'images';
        start_urls = [
            "http://www.acg.fi",
        ];
        count = 0;
        page = 1;
    
        ''' 三级爬取 '''
        ''' 该部分主要获取要取得的内容 '''
        def parse3(self, response):
            # 判断已经获取了多少张图片了
            # 如果获取了够3000张图片了,就返回
            if self.count >= 3000:
                return None;
            # 先获取item
            item = response.meta["item"];
            # 抓取图片的url
            image_url = response.xpath("//article[@class='article-content']//img/@src").extract();
            # 打印一共有多少个图片url
            print("一共找到图片%d张" % len(image_url));
            # 进行图片的抓取
            for url in image_url:
                url = urllib.request.quote(url,safe='/:?=.!');
                if "jpg" in url:
                    self.count += 1;
                    item["img_url"] = url;
                    item["img_name"] = "img"+str(self.count);
                    yield item;
                if "png" in url:
                    self.count += 1;
                    item["img_url"] = url;
                    item["img_name"] = "img"+str(self.count);
                    yield item;
    
    
        ''' 二级爬取 '''
        ''' 该爬取主要是将各栏目中的页面的url给爬取出来 '''
        def parse2(self, response):
            # 先获取item
            item = response.meta["item"];
            # 爬取该栏目下的分页情况
            pages_url = response.xpath("//div[@class='fenye']//a/@href").extract();
            # 这个num主要是为了产生各下级页面的url
            num = 1;
            # 先判断该栏目是否只有一个页面
            if len(pages_url)==0:
                # 如果是的话,把该栏目该页的url作为iterator的内容给parse3
                page = response.xpath("//h1[@class='article-title']//a/@href").extract_first();
                yield scrapy.Request(page, meta={"item":item}, callback=self.parse3);
            else:
                # 如果该栏目下有多页的话,则把各个页面的url取出来
                # 这里一定要进行切片操作,因为每次进来的都是第一页,而下面总会有一个
                # 按钮名曰“下一页”,这里就是要舍弃这个下一页
                for page in pages_url[:len(pages_url)-1]:
                    # 对该页面进行请求
                    yield scrapy.Request(page, meta={"item":item}, callback=self.parse3);
                    # 下级页面自加
                    num+=1;
            # 打印当前自己爬取了多少界面
            print(">>>  [parse2] -> 该栏目一共有%d个分页" % num);
    
    
        ''' 一级爬取 '''
        ''' 该爬取主要找到当前界面下有多少个栏目(二级界面) '''
        def parse1(self, response):
            # 首先获取item的值
            item = response.meta["item"];
            # 爬取该网站下的二级界面
            pages_url = response.xpath("//div[@class='card-item']//h3//a/@href").extract();
            # 打印出来找到了多少个二级页面
            print(">>>  [parse1] -> %s栏目的第%d页一共有%d个卡片" % (item["dir_name"],int(item["page_num"]),len(pages_url)));
            # 生成generator
            for url in pages_url:
                yield scrapy.Request(url,meta={"item":item}, callback=self.parse2);
            # 爬到网页数自加,由于网页数量可能比较多,因此不希望无限制的抓取下去
            if item["page_num"] < 20:
                item["page_num"]=item["page_num"]+1;
            print(">>>  [parse1] -> item's page_num is : %d" % item["page_num"]);
            # 找寻下个一级页面
            next_url = item["lm_url"]+"/page/"+str(item["page_num"]);
            # 这里只生成一个generator
            yield scrapy.Request(next_url,meta={"item":item},callback=self.parse1);
    
        ''' 初级爬取 '''
        ''' 该函数主要找到该网站下的各个分栏,之后给parse1 '''
        def parse(self, response):
            # 抓取分栏
            # 要拿到名称和链接
            lm_name = response.xpath("//li[contains(@id,'menu-item')]//a/text()").extract();
            lm_url = response.xpath("//li[contains(@id,'menu-item')]//a/@href").extract();
            # 去除首页和后面的分栏
            for idx in range(1,7):
                # 建立文件夹
                # 先确定要存储的位置
                path = settings.SAVE_PATH+lm_name[idx];
                # 判断是否有该位置的文件夹,没有则重新建立
                if not os.path.exists(path):
                    os.mkdir(path);
                # 打印栏目的名称和url
                print(">>>  [parse] -> 栏目名称 : %s  url : %s" % (lm_name[idx],lm_url[idx]));
                # 产生一个item
                item = AcgItem();
                item["dir_name"] = str(lm_name[idx]);
                item["lm_url"] = str(lm_url[idx]);
                item["page_num"] = 1;
                # 这里预留图像的url和img_name的值
                # 生成generator
                yield scrapy.Request(lm_url[idx], meta={"item":item}, callback=self.parse1);
    
    这里程序也比较简单,也带有我个人认为比较清楚的注释,我的建议是最好亲手撸一遍,过程中不懂的就baidu, bing, google,一般都能找到满意的回答。

    这里也着重说一下具有好感的Item,开始在接触这个框架的时候就在想如何在请求与请求之间传递参数,例如本例中的下载路径文件夹,这显然是要在不停的解析下传递下去的,看到Item我就放心了,Scrapy确实为我们想好了策略,而且这个Item还是字典的形式,各种取值赋值都很简单,这里主要还是提醒说在传递的时候,主要通过scrapy.Request中的meta变量进行传递,而且程序中也可以清晰的看到,传递的参数可以是一个,也可以是很多个的!只要把键值区分开就行。最后,一旦觉得实体可以给Pipeline了,直接yield一下该Item就行,太方便了。


    Item代码

    # -*- 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 AcgItem(scrapy.Item):
        # define the fields for your item here like:
        # name = scrapy.Field()
        # 文件夹的名称
        dir_name = scrapy.Field(serializer=str);
        lm_url = scrapy.Field(serializer=str);
        page_num = scrapy.Field(serializer=int);
        img_url = scrapy.Field(serializer=str);
        img_name = scrapy.Field(serializer=str);
    

    这里Scrapy的Field有一个很好的属性就是serializer,后面的配置可以限制这个变量的类型。


    Pipeline代码

    # -*- 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
    from acg.items import AcgItem;
    from acg import settings;
    from http.client import IncompleteRead;
    
    import urllib.error;
    import urllib.request;
    import urllib.parse;
    import numpy as np;
    import cv2;
    import os;
    
    class AcgPipeline(object):
    
        ''' 初始化函数 '''
        def __init__(self):
            self.img_seen = set();
    
        ''' 执行函数 '''
        def process_item(self, item, spider):
            # 判断是否见过这个image_name
            if item["img_name"] in self.img_seen:
                # 如果是的话打印出信息
                print(">>>  [pipeline] -> old url is used!");
                # 并返回
                return item;
            else:
                # 把图片给resize一下,防止过大
                maxSize = 512;
                # 获取url
                url = item["img_url"];
                # 模仿浏览器进行访问
                req = urllib.request.Request(url=url);
                req.add_header("User-Agent", "Mozilla/5.0 (Windows NT 6.1; rv:2.0.1) Gecko/20100101 Firefox/4.0.1");
                # req.add_header("GET",url);
                # req.add_header("Host", "img.gov.com.de");
                req.add_header("Referer", url);
                # 数据的获取
                try:
                    # 获取该数据
                    res = urllib.request.urlopen(req, timeout=100).read();
                    # 转化为numpy类型的数据
                    image = np.asarray(bytearray(res), dtype="uint8");
                    # 转化为opencv的数据
                    image = cv2.imdecode(image, cv2.IMREAD_COLOR);
                    # reszie
                    imgResize = cv2.resize(image, dsize=(maxSize, maxSize), interpolation=cv2.INTER_CUBIC);
                    # 存储
                    # 先确定要存储的位置
                    path = settings.SAVE_PATH+item["dir_name"];
                    # 判断是否有该位置的文件夹,没有则重新建立
                    if not os.path.exists(settings.SAVE_PATH):
                        os.makedirs(path);
                    # 生成图片保存的路径
                    img_path = path + "/" + item["img_name"] + ".jpg";
                    # 保存图片
                    cv2.imwrite(img_path, imgResize);
                    # 打印保存信息
                    print(">>>  [pipeline] -> image saves in %s" % img_path);
                except urllib.error.HTTPError as e:
                    print(">>>  [pipeline] -> HTTPError occur, it's code is %s" % str(e.code));
                except (IncompleteRead) as e:
                    print(">>>  [pipeline] -> IncompleteRead occur, it's code is %s" % str(e.code));
                except urllib.error.URLError as e:
                    print(">>>  [pipeline] -> URLError occur, it's code is %s" % str(e.code));
                # 别忘了返回item
                return item;
    
    这部分代码也没有特别多的地方需要讲,硬说的话主要是在urllib的urlopen时尽量加上timeout的设置。

    PS:如果要用Pipeline进行资源下载,一定要在setting里面把关于Pipeline的注解删掉并加上自己的Pipeline名称,例如本程序中是下面代码所示的样子,然后最后的数字是0~1000之间就行,表示优先级,具体的可以看注释的网址:D

    # Configure item pipelines
    # See https://doc.scrapy.org/en/latest/topics/item-pipeline.html
    ITEM_PIPELINES = {
       'acg.pipelines.AcgPipeline': 300,
    }


    最终结果

    Python初识——Scrapy抓取二次元小姐姐图片_第3张图片

    可以看到最后是完成了我们的愿想:D


    总结

    1. Python真心强大,这几天还在继续看一些基础的教程,发现很多很好用的语句和内置函数(太TM强大了,让在C和C++摸爬滚打的我十分佩服),还是很值得学习一下

    2. Scrapy真心强大,分布式的抓取,速度很快,但是看网上的建议说速度太快也不好,应该加个延时,但是我在跑程序的时候并没有遇到锁IP的情况,因此就没有加延时

    3. 整个程序里面有很多不太规范的地方,比如每句后面都加了分号,而且字符串大多数都用的是双影号,主要是为了和C++编程习惯一致,毕竟以后还要靠C++吃饭:x

    4. 最后是整个程序的代码,如果想作为参考的可以到GitHub上下载,链接:https://github.com/wuRDmemory/acg_spider


    以上的所有都是个人的理解,如果有错误,还请各位大神能够指点纠正,在此不胜感谢!


  • 你可能感兴趣的:(Python,Scrapy)