Python网络爬虫实战之十三:Scrapy爬取名侦探柯南漫画集

96
麦典威
0.1 2018.08.15 07:44* 字数 1713

目录:Python网络爬虫实战系列

正文:

本文爬取的是这个网站:hhttp://comic.kukudm.com/comiclist/5/

一、创建项目

在开始爬取之前,我们必须创建一个新的Scrapy项目。 进入打算存储代码的目录中,运行下列命令:

scrapy startproject conan

生成的工程结构如图:


conan1.jpg

二、shell分析

使用 Scrapy 内置的 Scrapy Shell 分析一下目标网页

1、分析《名侦探柯南》主页

查看网页的body信息

response.body

查看每个每个章节的链接和名字

response.xpath('//dd/a[1]')

输出结果如图:


conan2.jpg

从输出结果可以看到,每个链接都已经提取出来了,但是没有显示a标签里面的内容。想要显示全,就需要extract()方法,转换成字符串输出,指令如下:

response.xpath('//dd/a[1]').extract()

输出结果如图:


conan3.jpg

如果我想保存每个章节的图片,需要哪些东西?

链接必不可少,当然还有每个章节的名字,我们要以文件夹的形式存储每个章节,文件夹的命名就是章节的名字,这样更规整。

我们使用text()获取每个章节的名字,指令如下:

response.xpath('//dd/a[1]/text()').extract()

我们使用@href获取每个章节的链接,指令如下:

response.xpath('//dd/a[1]/text()').extract()

2、分析每个章节里的内容

分析每个章节里的内容,看看如何获取每个图片的链接。

从这个网页我们要获取哪些信息?第一个当然还是图片的链接,第二个呢?将一个章节里的每个图片保存下来,我们如何命名图片?用默认名字下载下来的图片,顺序也就乱了。仔细一点的话,不难发现,第一页的链接为:http://comic.kukudm.com/comiclist/5/3389/1.htm,第二页的链接为:http://comic.kukudm.com/comiclist/5/3389/2.htm,第三页的链接为:http://comic.kukudm.com/comiclist/5/3389/3.htm 依此类推,所以我们可以根据这个规律进行翻页,而为了翻页,首先需要获取的就是每个章节的图片数,也就是页数,随后,我们根据每页的地址就可以为每个图片命名:第1页、第2页、第3页…,这样命名就可以了。不会出现乱序,并且很工整,方便我们阅读。由于有的章节图片的链接不是规律的,所以只能先获取页面地址,再获取图片地址,这样递进爬取。

使用ctrl+c退出之前的shell,分析章节页面,以第一章为例,使用指令如下:

scrapy shell "http://comic.kukudm.com/comiclist/5/3389/1.htm"

通过审查元素可以知道,页数存放在valign属性i为top的td标签中。获取的内容由于有好多信息,我们再使用re()方法,通过正则表达式获取页数。获取页数代码如下:

response.xpath('//td[@valign="top"]/text()').re('共(\d+)页')[0]

下面该获取图片的链接了,通过审查元素我们会发现,图片链接保存再img标签下的src属性中,理想状态,使用如下指令就可以获取图片链接:

response.xpath('//img[@id="comipic"]/@src').extract()

返回为空。这是为什么?通过response.body打印信息不难发现,这个链接是使用JS动态加载进去的。直接获取是不行的,网页分为静态页面和动态页面,对于静态页面好说,对于动态页面就复杂一些了。可以使用PhantomJS、发送JS请求、使用Selenium、运行JS脚本等方式获取动态加载的内容。(该网站动态加载方式简单,不涉及这些)

该网站是使用如下指令加载图片的:

document.write("<img id=comicpic name=comicpic src='"+server+"kuku3comic3/mztkn/Vol_01/0015A025H.jpg'><span style='display:none'><img src='"+server+"kuku3comic3/mztkn/Vol_01/00258044I.jpg'></span>");

JS脚本放在网页里,没有使用外部JS脚本,这就更好办了,直接获取脚本信息,不就能获取图片链接了?使用指令如下:

response.xpath('//script/text()').extract()

通过运行结果可以看出,我们已经获取到了图片链接,server的值是通过运行JS外部脚本获得的,但是这里,我们仔细观察server的值为http://n5.1whour.com/,其他页面也是一样,因此也就简化了流程。同样,记住这个指令,编写程序的时候会用到。

思路已经梳理清楚,需要的内容有章节链接、章节名、图片链接、每张页数。shell分析完毕。

三、Scrapy程序编写

1、Spiders程序测试

在cortoon/spiders目录下创建文件comic_spider.py
先打印章节链接地址看看,代码如下:

import scrapy


class ComicSpider(scrapy.Spider):
    name = "comic"
    allowed_domains = ['comic.kukudm.com']
    start_urls = ['http://comic.kukudm.com/comiclist/5/']

    def parse(self, response):
        link_urls = response.xpath('//dd/a[1]/@href').extract()
        for each_link in link_urls:
            print('http://comic.kukudm.com' + each_link)

可使用如下指令运行工程来测试:

scrapy crawl comic

再打印章节名字看看,代码如下:

import scrapy


class ComicSpider(scrapy.Spider):
    name = "comic"
    allowed_domains = ['comic.kukudm.com']
    start_urls = ['http://comic.kukudm.com/comiclist/5/']

    def parse(self, response):
        # link_urls = response.xpath('//dd/a[1]/@href').extract()
        dir_names = response.xpath('//dd/a[1]/text()').extract()
        for each_name in dir_names:
            print(each_name)

2、Items编写

import scrapy


class ComicItem(scrapy.Item):
    dir_name = scrapy.Field()
    link_url = scrapy.Field()
    img_url = scrapy.Field()
    image_paths = scrapy.Field()

3、Settings编写

BOT_NAME = 'conan'

SPIDER_MODULES = ['conan.spiders']
NEWSPIDER_MODULE = 'conan.spiders'


# Crawl responsibly by identifying yourself (and your website) on the user-agent
#USER_AGENT = 'conan (+http://www.yourdomain.com)'

# Obey robots.txt rules
ROBOTSTXT_OBEY = False

ITEM_PIPELINES = {
    'conan.pipelines.ComicImgDownloadPipeline': 1,
}

IMAGES_STORE = 'D:/DataguruPyhton/PythonSpider/images/名侦探柯南'

COOKIES_ENABLED = False

DOWNLOAD_DELAY = 0.25    # 250 ms of delay
  • BOT_NAME:自动生成的内容,根名字;
  • SPIDER_MODULES:自动生成的内容;
  • NEWSPIDER_MODULE:自动生成的内容;
  • ROBOTSTXT_OBEY:自动生成的内容,是否遵守robots.txt规则,这里选择不遵守;
  • ITEM_PIPELINES:定义item的pipeline;
  • IMAGES_STORE:图片存储的根路径;
  • COOKIES_ENABLED:Cookie使能,这里禁止Cookie;
  • DOWNLOAD_DELAY:下载延时,这里使用250ms延时。

4、Comic_spider编写

import re
import scrapy
from scrapy import Selector
from conan.items import ComicItem


class ComicSpider(scrapy.Spider):
    name = 'comic'

    def __init__(self):
        # 图片链接server域名
        self.server_img = 'http://n5.1whour.com/'
        # 章节链接server域名
        self.server_link = 'http://comic.kukudm.com'
        self.allowed_domains = ['comic.kukudm.com']
        self.start_urls = ['http://comic.kukudm.com/comiclist/5/']
        # 匹配图片地址的正则表达式
        self.pattern_img = re.compile(r'\+"(.+)\'><span')

    # 从start_requests发送请求
    def start_requests(self):
        yield scrapy.Request(url=self.start_urls[0], callback=self.parse1)

    # 解析response,获得章节图片链接地址
    def parse1(self, response):
        hxs = Selector(response)
        items = []
        # 章节链接地址
        urls = hxs.xpath('//dd/a[1]/@href').extract()
        # 章节名
        dir_names = hxs.xpath('//dd/a[1]/text()').extract()
        # 保存章节链接和章节名
        for index in range(len(urls)):
            item = ComicItem()
            item['link_url'] = self.server_link + urls[index]
            item['dir_name'] = dir_names[index]
            items.append(item)

        # 根据每个章节的链接,发送Request请求,并传递item参数
        for item in items[-13:-1]:
            yield scrapy.Request(url=item['link_url'], meta={'item': item}, callback=self.parse2)

    # 解析获得章节第一页的页码数和图片链接
    def parse2(self, response):
        # 接收传递的item
        item = response.meta['item']
        # 获取章节的第一页的链接
        item['link_url'] = response.url
        hxs = Selector(response)
        # 获取章节的第一页的图片链接
        pre_img_url = hxs.xpath('//script/text()').extract()
        # 注意这里返回的图片地址,应该为列表,否则会报错
        img_url = [self.server_img + re.findall(self.pattern_img, pre_img_url[0])[0]]
        # 将获取的章节的第一页的图片链接保存到img_url中
        item['img_url'] = img_url
        # 返回item,交给item pipeline下载图片
        yield item
        # 获取章节的页数
        page_num = hxs.xpath('//td[@valign="top"]/text()').re(u'共(\d+)页')[0]
        # 根据页数,整理出本章节其他页码的链接
        pre_link = item['link_url'][:-5]
        for each_link in range(2, int(page_num) + 1):
            new_link = pre_link + str(each_link) + '.htm'
            # 根据本章节其他页码的链接发送Request请求,用于解析其他页码的图片链接,并传递item
            yield scrapy.Request(url=new_link, meta={'item': item}, callback=self.parse3)

    # 解析获得本章节其他页面的图片链接
    def parse3(self, response):
        # 接收传递的item
        item = response.meta['item']
        # 获取该页面的链接
        item['link_url'] = response.url
        hxs = Selector(response)
        pre_img_url = hxs.xpath('//script/text()').extract()
        # 注意这里返回的图片地址,应该为列表,否则会报错
        img_url = [self.server_img + re.findall(self.pattern_img, pre_img_url[0])[0]]
        # 将获取的图片链接保存到img_url中
        item['img_url'] = img_url
        # 返回item,交给item pipeline下载图片
        yield item

5、Pipelines编写

pipelines.py主要负责图片的下载,我们根据item保存的信息,进行图片的分类保存

from conan import settings
from scrapy import Request
import requests
import os


class ComicImgDownloadPipeline(object):
    def process_item(self, item, spider):
        # 如果获取了图片链接,进行如下操作
        if 'img_url' in item:
            images = []
            # 文件夹名字
            dir_path = '%s/%s' % (settings.IMAGES_STORE, item['dir_name'])
            # 文件夹不存在则创建文件夹
            if not os.path.exists(dir_path):
                os.makedirs(dir_path)
            # 获取每一个图片链接
            for image_url in item['img_url']:
                # 解析链接,根据链接为图片命名
                houzhui = image_url.split('/')[-1].split('.')[-1]
                qianzhui = item['link_url'].split('/')[-1].split('.')[0]
                # 图片名
                image_file_name = '第' + qianzhui + '页.' + houzhui
                # 图片保存路径
                file_path = '%s/%s' % (dir_path, image_file_name)
                images.append(file_path)
                if os.path.exists(file_path):
                    continue
                # 保存图片
                with open(file_path, 'wb') as handle:
                    response = requests.get(url=image_url)
                    for block in response.iter_content(1024):
                        if not block:
                            break
                        handle.write(block)
            # 返回图片保存路径
            item['image_paths'] = images
        return item

6、运行程序

scrapy crawl comic
Gupao