Python爬虫:二层爬虫之爬取整个相册

上次读了这篇文章的之后的老司机们可能发现了一个问题,我们爬取的妹子图片怎么都!是!封!面!。那是因为我们爬取得的内容是列表页提供的,所以只有封面图片链接。这并不能满足广大爬友的喜好,要来就要来全套。于是我们试图让我们的爬虫,让它能够爬取每个相册并保存没个相册在一个新建的文件夹中。

结构与模块分析

还记得上次我们的逻辑顺序是什么吗?

加载列表页->获取图片地址->下载图片->页面跳转

这次我们是一个两层的爬虫,所以要增加一些步骤

加载列表页->获取所有相册首页地址(需要初步页面跳转)->判断相册所有页面地址->从地址解析图片地址->创建文件夹并保存->跳转至下一相册

我们来拆开一步一步分析,首先来看我们的工具准备

工具准备

1
2
3
4
5
6
7
8
import urllib.request
import re
import time
import gzip
import os
header = {      "Accept-Encoding": "gzip",
                'User-Agent':'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6'}
girl_entry = []
  • 和上一次相比我们多了一个os模块,这个是后面创建文件夹所必需的
  • 我们把header变成全局变量,这是因为很多函数都需要调用这个变量。并且我们添加了gzip为可以接受的方式
  • girl_entry 是相册热口链接的集合,每次worker都会从里面取得一个链接

好了,上车吧。

获取所有相册入口地址

和我们上次的爬取过程一样,只不过我们爬取的内容不一样。上一次我们爬取的是图片链接,而这次我们爬取的是相册入口链接。这两者并没有特别大的区别,因为当你点击图片时就可以进入相册,这说明该链接和图片应该在同一个区块内(即你点击的其实是图片和链接的结合)。所以我们只需要更换我们的正则表达式就可以啦,下面这段代码是不是看着很眼熟呢,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
def get_entry_link():
        global girl_entry
        pages = int(input("Please enter the pages you want:"))
        page = 1
        while page <= pages:
                url = "http://www.youmzi.com/xg/list_10_%d.html"%page
                requests = urllib.request.Request(url,headers =header)
                opened = urllib.request.urlopen(requests)
                #UnicodeDecodeError
                decompress = gzip.GzipFile(fileobj = opened)
                content= decompress.read().decode('gbk')
                list_part_pattern=re.compile(r"</dl>(.*?)</ul>",re.S)
                list_part = re.findall(list_part_pattern,content)
                repattern = re.compile(r'<li>.*?<a href="(.+?).html" title="(.+?)".*?<p><a href.*?</li>',re.S)
                girl_link_found = re.findall(repattern,list_part[0])
                for each in girl_link_found:
                        girl_entry.append(each)
                page+= 1

尽管大部分内容一样,我们在这里还是做了一些细小的改变。

  • 由于这次我们将Gzip压缩的声明添加到了全局的header里面,所以服务器默认会给我们传输压缩后的数据,与上次不一样的是,我们在第10行添加了解压缩的操作过程。
  • 我们不难发现这段代码我们使用了两次正则表达式,因为如果只使用第14行的正则表达式的话,很容易将导航栏的li标签的内容和我们真正需要的内容混在一起,这时候我们需要对网页进行预先切片,先使用12,13行网页中的完整列表切出来再进行二次匹配可以提高精准度。要注意的是我们的list_part输出的内容是正则表达式输出的元组,所以第15行要使用[0]来表示其中的内容
  • 第二个正则表达式提取出来的内容有两个并以元组的方式展现 (一个去掉.html后缀的入口链接,一个标题)。入口链接是用在网页搜索中,标题名称是为我们创建的相册文件夹命名。

获取所有相册页面

当我们满怀欣喜地点击进入相册后,你发现了几个问题:

  • 每一个网页只有一张图片,旁边还有温馨提示,“点击图片进行下一页”,而我们并不能知道这个相册有多少页。 要不我们一个一个试吧,这样很浪费时间,如果中途出错了的话会影响后面的爬取。
  • 第一页的网址链接和其他的不一样,第一页为/12345.html,而其他的网页/12345_x.html。如果我们像之前一样使用/12345_1.html来代表第一页的话会直接报错

道高一尺魔高一丈,即便是这样我们仍然有办法:

创建一个空列表 -> 先把第一页放进去(解决问题二)-> 使用二分法搜索出尾页(解决问题一)-> 生成所有页面地址并加入列表 -> 返回列表

这就是下面这段代码的整体逻辑结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
def series_link(url_base):
        global header
        girl_series = []
        girl_series.append(url_base+'.html')
        begin = 2
        end = 100
        while begin+1 < end:
                mid = (begin+end)//2
                url = url_base+'_%d'%mid+'.html'
                try:
                        a = urllib.request.Request(url,headers= header)
                        b = urllib.request.urlopen(a)
                        ###
                        begin = mid
                except:
                        end = mid
        for each in range(2,begin+1):
                tmp_url = url_base+'_%d'%each+'.html' 
                girl_series.append(tmp_url)
        return girl_series
  • 首先我们要知道的是传入的入口链接并不是一个完整的链接,我们切除掉后缀是为了后面增加后缀时更加方便
  • 第3行 : 创建一个空列表
  • 第4行 : 先把第一页放进去(解决问题二)
  • 第5-16行是我们使用二分搜索的部分,由于有页码的页面从第二页开始,所以我们的起点是2,而这个相册基本不可能超过一百页,所以我们使用100作为上限。二分搜索的原理很简单,取起点终点的中间点,如果中间点在列表中,说明起点到中间点的所有内容都在列表内,这时我们已经可以排除掉一半了,反过来也是这样。所以我们使用这种方法可以提高效率节省资源。而测试中间点是否有效时使用try...except而不是if..else..因为当中间点使用urlopen方法打开时可能会报错。
  • 第18行:将我们获得的数字添加到base后面生成链接
  • 最后一行返回一个链接集合列表

获取图片地址

当然我们的最终目的还是要找到图片地址,有了我们之前的基础,这一步并不难实现。值得注意的是我们的这个函数作为一个图片链接搜索的整体

获得入口地址 -> 输出所有图片地址

所以我们引入了上一个函数。图片地址搜寻的方式不是很难,只有一张图片,所以对正则表达式的要求也不是很高

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
def pic_seeker(entry_link):
        link_pool = series_link(entry_link)
        pic_links = []
        num = 0
        for link in link_pool:
                req = urllib.request.Request(link,headers =header)
                oped = urllib.request.urlopen(req)
                decomp = gzip.GzipFile(fileobj = oped)
                cont = decomp.read().decode('gbk')
                pic_pattern = re.compile(r"<div class=\"arpic\">.*?<ul>.*?<li>.*?<img src='(.+?)'.*?</li>.*?</ul>",re.S)
                pic_link = re.findall(pic_pattern,cont)
                pic_links.append(pic_link[0])
                num+=1
                print(pic_link[0],num)
        return pic_links
  • 第2行引入series_link(entry_link)函数获得相册所有地址
  • 第5-13行遍历所有地址并执行抓取操作
  • 最后返回相册所有图片链接,到了这一步我们获得了我们需要的内容,只剩下下载了

保存至本地文件夹

虽然到了简单的最后一步我们仍然不能掉以轻心,因为这里有一个陷阱:有的图片被压缩了,有的并没有被压缩。如果我们直接下载的话会下载到一些损坏的文件,而直接使用gzip会报错,所以我们要使用try..except..。另外我们还需要将我们的妹子图保存在一个文件夹中以免和其他的混在一起,直接上代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def pic_downloader(pic_link_pool,series_name):
        if os.path.exists(os.path.join(os.path.dirname(__file__),'%s'%series_name)):
                return
        else:
                os.mkdir(os.path.join(os.path.dirname(__file__),'%s'%series_name))
        girl = 0
        for link in pic_link_pool:
                print('---link')
                a = open('%s/'%series_name+str(girl)+'.jpg','wb')
                try:
                        b = urllib.request.Request(link,headers = header)
                        c = urllib.request.urlopen(b)
                        d = gzip.GzipFile(fileobj = c)
                        e = d.read()
                except:
                        b = urllib.request.Request(link,headers = header)
                        c = urllib.request.urlopen(b)
                        e = c.read()
                a.write(e)
                print("No. %d Girl downloaded, - %s"%(girl,series_name))
                girl+=1
                a.close()                
                time.sleep(1)
  • 第2-5行我们使用了os模块对系统进行一定的操作,判断当前目录下是否有这个文件夹,如果有的话就跳过,没有的话就创建一个新的文件夹。由于我们使用了os模块,在运行我们的程序时需要管理员权限才能对系统进行操作。
  • 第10-18行我们默认使用gzip,因为使用urlopen并不能识别是否被压缩而直接下载全部内容

对相册下载器进行封装

其实有了之前的内容我们就已经可以完成我们的工作了,但是封装这一步并不是画蛇添足因为它的作用是减少代码的重复。这是一个很好的习惯,让一个函数完成一个完整的动作,这样我们在后面开发过程中调用更加方便。这里我们定义了一个worker,它整合了之前的几个函数,所以它可以接受一个入口链接并完成搜索下载的功能。这样做的好处是如果以后使用多线程的时候,每个线程添加一个worker就OK。

1
2
3
4
5
def worker():
    global girl_entry
    entry_base = girl_entry.pop(0)
    pic_link_pool=pic_seeker(entry_base[0])
    pic_downloader(pic_link_pool,entry_base[1])

要注意的是第2行的pop方法,它的作用是将列表中的元素传递出去并从列表中删除。这个原理有点像枪把子弹发射出去一样,每发射出去一颗子弹弹夹里面便少一颗。

主函数

主函数逻辑简单,创建一个入口集合,从里面获取链接交给worker工作

1
2
3
4
5
def main():
        global girl_entry
        get_entry_link()        
        while girl_entry:
            worker()

作为最接近人类语言的编程语言,我打算把上面的python代码直接翻译。

1
2
3
4
5
定义 主函数():
    全局变量(引入) girl_entry 
    (执行) get_entry_link() 
    当 girl_entry(不是空的):
        (反复执行) worker()

怎么样,是不是有了更深入的了解呢