开头:

【爬虫】【教程向】爬取壁纸网站Wallhaven收藏夹中的壁纸_收藏夹

Wallhaven是一个壁纸网站,注册账号后可以将喜欢的壁纸添加至收藏夹中。

里面有许多质量非常高的壁纸,为了方便下载,于是花了两天多时间写了一个爬壁纸的脚本(蒟蒻落泪)


需求:

首先明确自己的需求:

代码的功能应包含:

  • 访问各个收藏夹,按收藏夹的分类下载图片
  • 下载的图片保存至对应收藏夹名的文件夹中
  • 下载图片应按照网站中给出的编号来命名
  • 含有记录下载的功能,避免未全部下载完后关闭程序,第二次打开时从头开始下载的悲剧
  • 下载过程中有适当的提示,例如:正在下载xx收藏夹中的图片;第x张图片已下载完成等等

准备工作:

接着做一些准备工作:

最开始抱着试一试的心态找找看有没有api,居然发现这个网站提供了详细的api说明

良心,感动

【爬虫】【教程向】爬取壁纸网站Wallhaven收藏夹中的壁纸_创建文件夹_02

在api说明页面的最后面我们可以了解到:

通过这个链接:https://wallhaven.cc/api/v1/collections?apikey=<API KEY>(下文中称为1号链接)可以访问自己的所有收藏夹信息

再通过这个链接:https://wallhaven.cc/api/v1/collections/USERNAME/ID(下文中称为2号链接)可以访问某个收藏夹中所收藏的全部壁纸的详细信息

注:若某个收藏夹被你设置为私密,访问详情时只需要在后面加上?apikey=<API KEY>便可以了。API KEY可以通过查看个人信息的设置页面找到。


思路:

以上是爬图片前做的准备工作,下面便开始构思思路了:

1号链接:

打开1号链接可以看见:

{

"data":

[

{"id":9xxxx8,"label":"Nxx","views":0,"public":0,"count":0},

{"id":9xxxx4,"label":"Xxxxx","views":4,"public":0,"count":126},

{"id":9xxxx7,"label":"Xxxxxxng","views":0,"public":0,"count":22},

{"id":9xxxx6,"label":"Xixxxxxxon","views":0,"public":0,"count":9},

{"id":9xxxx8,"label":"Nxxxxo","views":0,"public":1,"count":10},

{"id":9xxxx9,"label":"Nxxxxxian","views":0,"public":1,"count":33},

{"id":9xxxx7,"label":"Nxxxxx","views":2,"public":1,"count":47},

{"id":9xxxx0,"label":"Sxxxxn","views":2,"public":1,"count":24},

{"id":9xxxx2,"label":"Sxxxxxe","views":0,"public":1,"count":7}

]

}

注:直接访问得到的内容是没有换行了,为了方便展示我加了换行符,下面也是一样

得到的是一个只有一个键值对的字典,键名是data,值是一个列表,列表里又套了多个字典,字典中包含了各个收藏夹的详细信息,包括:收藏夹的id、名称等等,重要的就是id和名称这两项内容了。

2号链接:

接下来根据id访问2号链接:

{

"data":

[

{"id":"9mz31w",

--snip--

"path":"https:\/\/w.wallhaven.cc\/full\/9m\/wallhaven-9mz31w.png",

--snip--

}},

--snip--

{"id":"lqzp62",

--snip--

"path":"https:\/\/w.wallhaven.cc\/full\/lq\/wallhaven-lqzp62.jpg",

}}

],

"meta":{"current_page":1,"last_page":2,"per_page":24,"total":47}

}

得到的同样也是一个字典,有两个键,一个是data,另一个是meta,主要用的是data。第一个键data的值是一个列表,列表中又套了多个字典,分别包含了各张图片的详细信息,我们只需要里面path键的值就可以了。

 

看完了上面两个页面,再根据下载的需求便有了一个大致的思路:

首先访问1号链接,得到各个收藏夹的id与名称即label,接着根据得到的id,分别访问2号链接,得到图片的链接即path,访问path即可得到原图


代码详情:

有了思路便开始正式写代码:

首先导入所需要的运行库:

import requests
import os
import json
import re
from time import sleep

再定义一个Wallhaven_master类,再定义若干个属性:

def __init__(self):
        super(Wallhaven_master, self).__init__()
        self.url_user = 'https://wallhaven.cc/api/v1/collections?apikey=<API KEY>'#用的时候需要填写自己的api key
        self.headers = {
            "User-Agent": "",#内容可通过自己的浏览器查看,这里我删了
        }
        self.name = []  # 列表,储存每个收藏夹的label
        self.colle_id = []  # 列表,储存每个收藏夹的id
        self.pic_id = {}  # 字典,键名为收藏夹id,键值为列表,储存每个收藏夹下所有图片path
        self.DLed = []  # 列表,储存此次运行过程中已下载的图片path
        self.DLread = []  # 列表,储存从json文件从读取的已下载的图片path,用于排重
        self.DLtmp = []#列表,储存从json中读取的已下载链接

接下来写若干个函数(方法):

def get_data(self, a):
        '''
        打开api ,返回 data 字典
        '''
        r = self.get(a, headers=self.headers)
        req = r.json()
        data = req['data']
        return data
      
#备注:通过访问前面的两个链接可以发现有个巧合:都是个字典,且都有键data,里面都保存了我们需要的信息
#	   因为这个巧合,为了方面我们的访问,减少代码量便可以写这个函数


    def get_colle_name(self):
        '''
        获得每个收藏夹的名字 label
        '''
        data = self.get_data(self.url_user)
        for x in data:
            name = x['label']
            self.name.append(name)
            
#备注:将收藏夹的名称添加至列表self.name[]中


    def get_colle_id(self):
        """
            获取每个收藏夹的id
            在列表 pic_id 中添加 列表元素 colle_id
        """
        data = self.get_data(self.url_user)
        for x in data:
            colle = x['id']
            self.colle_id.append(colle)

    def get_pic_id(self):
        '''
        获得每个图片的 path
        将 path 添加至字典 self.pic_id 各个键的值(列表)中
        '''
        for x in self.colle_id:
            url = f"https://wallhaven.cc/api/v1/collections/NAME/{x}?apikey=<API KEY>"
            #备注:NAME是自己的用户名,api key前面有介绍
            #      这里我把自己的信息删了
            data = self.get_data(url)
            self.pic_id[x] = []
            for y in data:
                path = y['path']
                self.pic_id[x].append(path)

    def cret_floder(self):
        '''
        根据收藏夹名称创建文件夹
        '''
        if not os.path.exists('WH-pics'):
            os.mkdir("WH-pics")
        os.chdir('WH-pics')
        for x in self.name:
            if not os.path.exists(x):
                os.mkdir(x)

    def run(self):
        '''
        主程序:下载模块
        '''
        num = 0
        for x in self.pic_id.values():
            print(f'正在下载收藏夹 {self.name[num]} 中的图片')
            rest = len(self.pic_id[self.colle_id[num]])
            if not rest:
                print('此收藏夹中图片已全部下载完成')
            th = 1
            for y in x:
                name = y.split('/')[-1]
                r = self.get(y)
                #备注:此处删除了下载图片的代码
                self.DLed.append(y)
                self.loadjson()
                print(f"第 {th} 张图片已下载完成!")
                th += 1
                sleep(2.5)
            print('\n')
            num += 1
        print('所有收藏夹中的所有图片已全部下载完成!')

    def readjson(self):
        '''
        读取json文件中的已下载链接,若无则创建
        '''
        with open('DLed.json', 'a+') as dl:
            xyz = 0
        try:
            with open('DLed.json', 'r') as dl:
                self.DLread = json.load(dl)
        except ValueError:
            self.DLread = []

    def loadjson(self):
        '''
        将运行过程中下载的图片链接和从json文件中读取的已下载链接合并至一个列表中
        每下载一张图片,更新一次json文件,确保保存了下载记录,防止重复下载
        '''
        self.DLtmp = self.DLed + self.DLread
        with open('DLed.json', 'w+') as dl:
            json.dump(self.DLtmp, dl)

    def delrepeat(self):
        ''''
        删除重复的下载链接,避免重复下载
        '''
        for x in self.pic_id.values():
            for y in self.DLread:
                if y in x:
                    x.remove(y)

 以上便是程序的主要内容了,运行的话只需要创建一个类Wallhaven_master的实例,再依次使用上面写了的函数(方法)便可以了。

运行:

self.get_colle_id()
self.get_colle_name()
self.cret_floder()
self.get_pic_id()
self.readjson()
self.delrepeat()
self.run()
self.loadjson()

排重模块思路:

最后再说一下如何避免重复下载的思路:

创建一个json文件,里面用于存储之前已下载的图片的链接即path,

每次运行程序前读取里面的内容,在已获得的全部下载链接中,遍历删除读取的内容,之后再开始下载,

接着,每下载一张图片,便将该图片的链接添加至列表DLed中,

紧接着,合并列表DLed和列表DLread(即从json文件中读取的列表),接着在json文件中覆盖写入新列表,这样便确保了每下一张图片就可以把下载好的链接写入至json文件中来记录。

特别注意:

从官方的api介绍文档中得知:

API calls are currently limited to 45 per minute. If you do hit this limit, you will receive a 429 - Too many requests error.
Attempting to access a NSFW wallpaper without an API Key or with an invalid one will result in a 401 - Unauthorized error.
Any other attempts to use an invalid API key will result in a 401 - Unauthorized error.

每分钟请求数不得超过45次,所以使用sleep函数每下载一张图片便休息2s左右便可以完全满足需要


写在最后:

代码缺陷:

运行完代码后发现怎么每个收藏夹中下载的图片最多只有24张?!又看了api介绍文档后才发现,访问收藏夹详情只能得到前24张图片的详情。按理说在2号链接上加上?page=x是应该会解决的,但是我尝试了没有用,在网上查资料也没有找到解决办法,这个问题至今未解决。所以只能手动把收藏夹拆分成多个收藏夹,下载完后再手动合并,将就着用吧....蒟蒻落泪

其他:

自己一开始是想在网上找找有没有现成的直接拿来用,但是看了一圈发现都和我想要的没有半毛钱关系.....遂自己动手写了一个。

u1s1,在网上找现成的过程中,发现他们用的都是分析网页的方法,我就没看见一个利用官方api的,迷惑....

说实话写这个是我第一次写的一个“大” 程序,参考了一本python的入门介绍书,然后对着一个爬B站相册的代码照猫画虎的写出来了。

在写的过程中掉进了好多坑,比如说在使用列表元素的值的时候没有加索引,报错提示说:TypeError: unhashable type: 'list',看不懂,花了一晚上时间死活没想通.....蒟蒻落泪

还有,得到收藏夹名称时,访问的键应该是label,写的时候写成了lable.......

以上