開頭:
Wallhaven是一個壁紙網站,注冊賬號后可以將喜歡的壁紙添加至收藏夾中。
里面有許多質量非常高的壁紙,為了方便下載,於是花了兩天多時間寫了一個爬壁紙的腳本(蒟蒻落淚)
需求:
首先明確自己的需求:
代碼的功能應包含:
- 訪問各個收藏夾,按收藏夾的分類下載圖片
- 下載的圖片保存至對應收藏夾名的文件夾中
- 下載圖片應按照網站中給出的編號來命名
- 含有記錄下載的功能,避免未全部下載完后關閉程序,第二次打開時從頭開始下載的悲劇
- 下載過程中有適當的提示,例如:正在下載xx收藏夾中的圖片;第x張圖片已下載完成等等
准備工作:
接着做一些准備工作:
最開始抱着試一試的心態找找看有沒有api,居然發現這個網站提供了詳細的api說明:api說明頁面
良心,感動
在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.......
以上