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浅析

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

可以看到,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,随后我们查看他的源代码,发现这个网站的源代码真心的乱啊,一点儿都不归正:|不过没有关系,我们可以用检查的模式进行代码的浏览,很快就能找到我们想要解析的地方如下图所示

可以看到我们主要要解析的标签为<li>下的<a>标签,通过总结我们发现<li>标签下的id属性里都有“menu-item”这样的字段,很方便的就可以区分与其他的<li>标签,这时候我们就可以编写代码了,这里主要用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,
}


最终结果

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


总结

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

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

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

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


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


  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值