为了账号安全,请及时绑定邮箱和手机立即绑定

通过Python爬虫抓取漫画图片

标签:
Python

无聊浏览某漫画网站(你懂的。-_-),每次翻页时都需要重新请求整个页面,页面杂七杂八的内容过多,导致页面加载过程耗时略长。于是决定先把图片先全部保存到本地。本文的主要内容,就是讲解如何通过一个爬虫程序,自动将所有图片抓取到本地。

先来看网页大概张什么样。:)

漫画封面列表界面:

1.jpg

每部漫画点击进去的界面

2.jpg

漫画列表每一页有几十部漫画,网站暂时有15页列表,每部漫画页数有10+页到近200页不等,所以所有漫画包含的图片总数还是比较可观的,通过手动将每张图右键另存基本不可能完成。接下来将一步一步展示,如何用Python实现一个简单爬虫的程序,将网页上所有漫画全保存到本地。Python用的是2.7版本。

<h4>首先,何为网络爬虫?</h4>

网络爬虫(又被称为网页蜘蛛,网络机器人),是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本。

一般而言,简单的爬虫实现主要分为两步:

  • 获取指定网页html源码;

  • 通过正则表达式提取出源码中的目标内容。

<h3>本文程序实现大体步骤:</h3>

  1. 获取每页漫画列表的url;

  2. 获取每页漫画列表中每一部漫画的url;

  3. 获取每部漫画每一页图片的url;

  4. 通过图片的url将图片保存到本地。

(总之就是通过for循环遍历的过程。)

最后会附本文Demo完整源码。

<h3>获取每页漫画列表的url</h3>

程序最开始处理的页面为:http://www.xeall.com/shenshi,即漫画列表的首页。绅士,你懂的。。

先创建一个文件Gentleman.py,引入需要用到的库:

#coding:utf-8import urllib2import reimport osimport zlib

定义一个类,和初始化函数:

class Gentleman:
    def __init__(self, url, path):
        exists = os.path.exists(path)        if not exists:
            print "文件路径无效."
            exit(0)        self.base_url = url        self.path = path

url为漫画列表首页url

path为图片保存在本地的路径,做个错误检测,判断本地是否存在对应的路径

接下来定义 get_content 方法,获取指定url的html源码内容:

def get_content(self, url):
    # 打开网页
    try:
        request = urllib2.Request(url)
        response = urllib2.urlopen(request, timeout=20)        # 将网页内容解压缩
        decompressed_data = zlib.decompress(response.read(), 16 + zlib.MAX_WBITS)        # 网页编码格式为 gb2312
        content = decompressed_data.decode('gb2312', 'ignore')        # print content
        return content    except Exception, e:        print e        print "打开网页: " + url + "失败."
        return None

urllib2.Request() 通过指定的url构建一个请求,urllib2.urlopen() 获取请求返回的结果,timeout设为20秒无响应则做超时处理。urllib2.urlopen() 有可能出现打开网页错误的情况,所以做了异常处理,保证在返回40X50X之类的时候程序不会退出。后续相关的操作也都会做异常的处理。

原网页为压缩过的,请求到之后必须先进行解压缩处理。

通过 charset=gb2312 可知解码所需格式。

判断网页是否为压缩的编码类型,可以通过打印语句:

response.info().get('Content-Encoding')

打印结果为:

'gzip'

函数的返回结果为页面html源码文本。

跑起来试试:

url = "http://www.xeall.com/shenshi"save_path = "/Users/moshuqi/Desktop/cartoon"gentleman = Gentleman(url, save_path)
content = gentleman.get_content(url)print content

可以看到打印结果:

3.jpg

接下来分析源码文件,看如何从中获取到每一页漫画列表的url。

页面上有个选择页的控件:

4.jpg

通过分析对应控件的源码,可知具体每一页所对应的url:

5.jpg

控件的 namesldd ,通过搜索全文发现只有这一处“sldd”字段,所以该字段可用来做标识。

option value 的值即为对应页的url。

定义 get_page_url_arr 方法,获取每一页的url,返回一个数组:

def get_page_url_arr(self, content):
    pattern = re.compile('name=\'sldd\'.*?>(.*?)</select>', re.S)
    result = re.search(pattern, content)
    option_list = result.groups(1)

    pattern = re.compile('value=\'(.*?)\'.*?</option>', re.S)
    items = re.findall(pattern, option_list[0])

    arr = []    for item in items:
        page_url = self.base_url + '/' + item
        arr.append(page_url)

    print "total pages: " + str(len(arr))    return arr

传入的 content 为页面源码,首先通过 sldd 获取到其中 option 的内容。采用正则表达式将内容提取出来。(关于正则表达式,不熟悉的同学推荐看这本 【正则表达式必知必会】,很薄的一本书,半天时间大概翻一遍基本就能用来处理大部分常见问题了)

正则表达式用了Python re模块,具体方法的使用请自行百度, 这里只大概说一下思路。

首先用到的匹配模式:

'name=\'sldd\'.*?>(.*?)</select>'

'' 的转义字符,匹配先找到以 name='sldd' 开头的字符,.?* 是一个非贪婪匹配,用来匹配 name='sldd' 之后到最近的一个 > 之间的内容。(.?)* 意义和前一个类似,加上 () 表示为分组,可以在匹配结果中访问,即我们需要识别出的内容。结尾的 select 标签即为识别内容最后的标记。

运行后 option_list 识别到的内容应为:

<option value='p1.html' selected>1</option><option value='p2.html'>2</option><option value='p3.html'>3</option><option value='p4.html'>4</option><option value='p5.html'>5</option><option value='p6.html'>6</option><option value='p7.html'>7</option><option value='p8.html'>8</option><option value='p9.html'>9</option><option value='p10.html'>10</option><option value='p11.html'>11</option><option value='p12.html'>12</option><option value='p13.html'>13</option><option value='p14.html'>14</option><option value='p15.html'>15</option>

再正对 option_list 的内容进行识别,获取到每个 option 的值,匹配模式为:

'value=\'(.*?)\'.*?</option>'

取到的值和一开始 base_url 连接拼接起来即为每一页的url。

测试一下:

arr = gentleman.get_page_url_arr(content)print arr

打印结果:

[u'http://www.xeall.com/shenshi/p1.html', u'http://www.xeall.com/shenshi/p2.html', u'http://www.xeall.com/shenshi/p3.html', u'http://www.xeall.com/shenshi/p4.html', u'http://www.xeall.com/shenshi/p5.html', u'http://www.xeall.com/shenshi/p6.html', u'http://www.xeall.com/shenshi/p7.html', u'http://www.xeall.com/shenshi/p8.html', u'http://www.xeall.com/shenshi/p9.html', u'http://www.xeall.com/shenshi/p10.html', u'http://www.xeall.com/shenshi/p11.html', u'http://www.xeall.com/shenshi/p12.html', u'http://www.xeall.com/shenshi/p13.html', u'http://www.xeall.com/shenshi/p14.html', u'http://www.xeall.com/shenshi/p15.html']

<h3>获取每一页包含的漫画的url</h3>

例如,先对第一页做处理http://www.xeall.com/shenshi/p1.html

打开网页,找到这部分所对应的源码:

6.jpg

浏览器上展示这部分的源码很长,而且没有换行,我们可以将文本拷贝到本地的编辑器上再进行分析。

可以看到内容比较长,以下两张图分别是开头和结尾的截图,红色圈出的内容可作为识别这段文本的开头和结尾。

7.jpg

8.jpg

定义 get_cartoon_arr 方法,获取每一页包含的漫画的url,返回一个数组:

def get_cartoon_arr(self, url):
    content = self.get_content(url)    if not content:
        print "获取网页失败."
        return None

    pattern = re.compile('class="piclist listcon".*?>(.*?)</ul>', re.S)
    result = re.search(pattern, content)
    cartoon_list = result.groups(1)

    pattern = re.compile('href="/shenshi/(.*?)".*?class="pic show"', re.S)
    items = re.findall(pattern, cartoon_list[0])

    arr = []    for item in items:
        # print item
        page_url = self.base_url + '/' + item
        arr.append(page_url)    return arr

匹配模式:

'class="piclist listcon".*?>(.*?)</ul>'

识别出漫画列表的内容,内容太多就不打印了。

下图为每一部漫画所包含的信息,例如展示的第一部漫画为圈出的部分。

9.jpg

我们只需要提取到 href 中的信息,可以分别用 /shenshi/class="pic show" 作为开头结尾,提取出 “10444.html” 链接信息。

匹配模式为:

'href="/shenshi/(.*?)".*?class="pic show"'

将提取的结果和 base_url 连接拼接起来即为漫画的url。

测试代码:

arr = gentleman.get_cartoon_arr("http://www.xeall.com/shenshi/p1.html")print arr

打印结果:

[u'http://www.xeall.com/shenshi/10444.html', u'http://www.xeall.com/shenshi/10440.html', u'http://www.xeall.com/shenshi/10423.html', u'http://www.xeall.com/shenshi/10414.html', u'http://www.xeall.com/shenshi/10406.html', ...]

<h3>创建Cartoon类</h3>

用来专门处理每一部漫画的类,要处理的细节较多,所以专门封装成一个类来实现。

类的初始化函数,参数 url 为漫画地址

#coding:utf-8import urllib2import reimport zlibimport osclass Cartoon:
    def __init__(self, url):
        self.base_url = "http://www.xeall.com/shenshi"
        self.url = url

Cartoon类中也会包含 get_content 函数,和之前的实现方式一样,这里就不列出来了。当然,好的实现方式应该避免重复代码,我懒得整理直接拷过来了。- -

定义获取漫画名的方法 get_title ,因为漫画名后面要用来作为保存每部漫画的文件夹名称。

def get_title(self, content):
    pattern = re.compile('name="keywords".*?content="(.*?)".*?/', re.S)
    result = re.search(pattern, content)    if result:
        title = result.groups(1)        return title[0]    else:        print "获取标题失败。"
        return None

我们用这部漫画做测试http://www.xeall.com/shenshi/10444.html

测试代码:

title = cartoon.get_title(content)print title

打印结果:

绅士漫画:CURE UP秘密的宝岛 (魔法少女同人志)

接下来需要获取到翻页对应的url,翻页部分见下图:

11.jpg

定义获取漫画每一页的url方法 get_page_url_arr

def get_page_url_arr(self, content):
    pattern = re.compile('class="pagelist">(.*?)</ul>', re.S)
    result = re.search(pattern, content)
    page_list = result.groups(1)

    pattern = re.compile('<a href=\'(.*?)\'>.*?</a>', re.S)
    items = re.findall(pattern, page_list[0])

    arr = []    for item in items:
        page_url = self.base_url + "/" + item
        arr.append(page_url)    # pagelist中还包含了上一页和下一页,根据网页格式可知分别在开始和结束,所以去掉首尾元素避免重复
    arr.pop(0)
    arr.pop(0)
    arr.pop(len(arr) - 1)    return arr

打开网页源码:

10.jpg

可以看到圈出部分为每一页按钮对应的信息。

通过匹配模式获取到这部分内容;

'class="pagelist">(.*?)</ul>'

对这部分内容进行匹配,获取每一页的url:

'<a href=\'(.*?)\'>.*?</a>'

看下图会发现,前两个链接为 href='#' ,最后一个为下一页链接,重复了。

12.jpg

所以得到数组把这3个元素剔除掉。(至于为什么把 href='#' 都去掉,因为用这个链接后面会出错,具体原因懒得追究,反正去掉之后也就少了第一页的封面而已。)

arr.pop(0)arr.pop(0)arr.pop(len(arr) - 1)

测试代码:

arr = cartoon.get_page_url_arr(content)print arr

打印结果:

[u'http://www.xeall.com/shenshi/10444_2.html', u'http://www.xeall.com/shenshi/10444_3.html', u'http://www.xeall.com/shenshi/10444_4.html', ...]

<h3>获取每页漫画图片的url</h3>

例如针对之前获取到数组的第一个元素:http://www.xeall.com/shenshi/10444_2.html

打开网页,查看源码:
![Uploading 14_014335.jpg . . .]

13.jpg

找到页面图片对应的代码,通过搜索 img alt 可知文件中只有一处,所以可以用这个字段用来标识。

定义获取图片url的函数 get_pic_url

def get_pic_url(self, page_url):
    content = self.get_content(page_url)    if not content:        return None

    pattern = re.compile('<img alt.*?class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="(.*?)".*?/>', re.S)
    result = re.search(pattern, content)    if result:
        pic = result.groups(1)        return pic[0]    else:        print "获取图片地址失败。"
        print "url: " + page_url        return None

匹配模式为:

'<img alt.*?class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="(.*?)".*?/>'

这里最初使用了后面的 p 标签结束符作为结束标识,后来发现极少数部分页面用的不是 p 而是 br,因而导致图片url获取失败。

测试代码:

url = cartoon.get_pic_url("http://www.xeall.com/shenshi/10444_2.html")print url

打印结果:

http://tu.zzbzwy.com/xeall/uploadfile/gx02/160904/ww02.jpg

点击打开看到直接就是一张图片

14.jpg

<h3>将图片保存到本地</h3>

定义保存函数的方法 save_pic

def save_pic(self, pic_url, path):
    req = urllib2.Request(pic_url)
    req.add_header('User-Agent', 'Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36')
    req.add_header('GET', pic_url)    try:        print "save pic url:" + pic_url
        resp = urllib2.urlopen(req, timeout=20)
        data = resp.read()        # print data

        fp = open(path, "wb")
        fp.write(data)
        fp.close        print "save pic finished."
    except Exception, e:        print e        print "save pic: " + pic_url + " failed."

pic_url 为图片链接,path 为图片保存到本地的路径。

这遇到了个坑,Request 请求若不设置 User-Agent 信息会导致返回 403 Forbidden,服务器可能对访问做了些限制。

最后Cartoon类定义一个对外的 save 方法,将漫画所有图片保存到本地

def save(self, path):
    dir_path = path + "/" + self.title    self.create_dir_path(dir_path)    for i in range(0, len(self.page_url_arr)):
        page_url = self.page_url_arr[i]
        pic_url = self.get_pic_url(page_url)        if pic_url == None:
            continue

        pic_path = dir_path + "/" + str(i + 1) + ".jpg"
        self.save_pic(pic_url, pic_path)

    print self.title + " fetch finished."

给漫画创建创建本地文件夹,文件夹名称为漫画名

def create_dir_path(self, path):
    # 以漫画名创建文件夹
    exists = os.path.exists(path)    if not exists:        print "创建文件夹"
        os.makedirs(path)    else:        print "文件夹已存在"

完成的初始化函数:

 def __init__(self, url):    self.base_url = "http://www.xeall.com/shenshi"
    self.url = url

    content = self.get_content(self.url)    if not content:
        print "Cartoon init failed."
        return

    self.title = self.get_title(content)    self.page_url_arr = self.get_page_url_arr(content)

下载一部完整漫画的测试代码:

url = "http://www.xeall.com/shenshi/10444.html"save_path = "/Users/moshuqi/Desktop/cartoon"cartoon = Cartoon(url)
cartoon.save(save_path)

运行起来,看看置顶文件夹结果:

15.jpg

<h3>将Cartoon类和Gentlman类结合,爬取所有漫画图片</h3>

Gentleman 完整的初始化函数:

def __init__(self, url, path):
    exists = os.path.exists(path)    if not exists:
        print "文件路径无效."
        exit(0)    self.base_url = url    self.path = path
    content = self.get_content(url)    self.page_url_arr = self.get_page_url_arr(content)

外部调用的接口方法,遍历所有页和所有漫画:

def hentai(self):    # 遍历每一页的内容
    for i in range(0, len(self.page_url_arr)):        # 获取每一页漫画的url
        cartoon_arr = self.get_cartoon_arr(self.page_url_arr[i])
        print "page " + str(i + 1) + ":"
        print cartoon_arr        for j in range(0, len(cartoon_arr)):
            cartoon = Cartoon(cartoon_arr[j])
            cartoon.save(self.path)
        print "======= page " + str(i + 1) + " fetch finished ======="

最终的结果跑起来:

url = "http://www.xeall.com/shenshi"save_path = "/Users/moshuqi/Desktop/cartoon"gentleman = Gentleman(url, save_path)
gentleman.hentai()

打印输出:

16.jpg

可以看到置顶的文件夹里不断生成新的漫画文件夹和图片。

<h3>图片爬取结果</h3>

17.jpg


18.jpg


19.jpg

程序连续跑了几个小时,一共抓取500+部漫画,1W5+张图片,文件总大小4G+。大概如上图所示。- -

<h3>其他</h3>

程序里还做了些其他处理。偶尔会出现图片请求失败,导致一部漫画缺少几页,对于这种情况的漫画做处理时,通过判断只对缺失的页做请求。

重新跑程序时,每部漫画都会先拿服务器上的总页数与本地页数做对比,若大于或等于(为什么不是等于,因为Mac系统会在文件夹里生成.DS_store文件。。)则说明该部漫画已爬取完成,不再重新做处理。

总之就是避免重新运行程序时避免重复数据的爬取。处理在Demo代码里面,已加上注释说明。

<h3>最后。</h3>

本文只是展示了网络爬虫基本使用的大体思路而已,程序可以优化的地方还很多,如爬取时通过多线程,或用现成的爬虫框架之类等等等。读者请自行思考完善。当然,如果你想留邮箱的话。。不先去start下?:)

本文完整源码

原文地址

完。


作者:msq3
链接:https://www.jianshu.com/p/f91baf9b8926


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消