Python多线程下载...

python多线程下载图片(setu)

前言

python的学习其实早在两三个星期前就开始了,奈何我是个急性子,所以学的很浮躁,甚至连md笔记都没写。但是想了想,我学python的目的反正也仅仅是为了ctf的比赛。所以我直接去学习了对ctf有用的部分。例如,requests库、re库、threading库。所以这章的内容也就是我这几周研究的成果了。

那么进入正题。这章是对学习多线程的一个交代。

说来也好笑,激发我学习python的动力之一居然是setu。

由于ctf男♂童交流群有个bot,其中有个功能是——“来份色图”

这就很nb了。虽然我做不到监听QQ消息,制作出QQbot的程度,但是我可以找学长py一个setu接口呀~

所以拿到了api的我,开始了爬取setu的艰苦奋斗之路!

如果是想要最终版本的多线程爬取setu脚本的话,直接下拉到最后即可!

另附setu API

Version 01

这是我的第一个爬取图片脚本。

当时是第一次写python的下载脚本。

刚刚学会用with打开文件的我,迫不及待的开始了第一版本的脚本撰写~

import requests
import time
for i in range(1,300):
    print('start')
    url = 'https://api.lolicon.app/setu/?r18=2'
    res = requests.get(url)
    data = res.json()
    data = data['data'][0]
    setu_url = data['url']
    setu_id = data['uid']
    with open ('C:/Users/木鲸/OneDrive/setu_url.txt',"a") as file :
        file.write(setu_url+'\n')
    try:
        res = requests.get(setu_url).content
    except:
        res = requests.get(setu_url).content
    else:
        pass
    with open('C:/Users/木鲸/OneDrive/setup/'+str(setu_id)+".jpg","wb") as f:
        f.write(res)
    print('over')
    time.sleep(2)

当时写的想法就是,先访问api接口,爬取json的数据中的url和uid。 url用来下载图片,为了避免重复下载同样的,用uid来当作图片的名字。 我是将图片下载到本地的OneDrive中。因为当时用heroku搭建了一个每个月500小时免费的onemanager下载站。这样在下载站上看图片也比较方便。

但是这个版本的脚本可以说是一坨屎,根本没有什么技术可言,就仅仅是爬取了url进行下载。也没有进行错误处理。在当时这个api接口只有每日300次的时候,这个脚本可以说是非常浪费资源了。

Version 02

接下来来到了第二个版本

这个版本相较于01版本,多了一个错误处理。

原理就是,每次连接api接口的时候,将其中的url写入一个txt中。

然后调用处理txt的脚本来下载文件。

而下载失败的url会存入一个err_txt中,在下载完成之后,我只需要打开errtxt进行下载就行。

那么放下面这两个脚本

下载setu_url.txt中的图片内容

import requests
import time

data = []
err = []
err_txt = []
# 打开存着url的txt文件,将url全部读入data中
with open ('C:/Users/木鲸/OneDrive/setu_url.txt','r') as f:
    data = f.read().splitlines()
# 开始下载url中的图片,保存在本地的OneDrive中
for i in range(0,len(data)):
    print('Start! Ready to count!')
    startTime = time.time()
    url = data[i]
    try:
        print('Trying to connect!')
        res = requests.get(url).content
    except:
        err.append(url)
        print("error!")
        continue
    outpath = time.strftime("%Y%m%d%H%M%S", time.localtime())
    with open('C:/Users/木鲸/OneDrive/setup/'+(outpath)+".jpg","wb") as file:
        file.write(res)
    endTime = time.time()
    runTime = endTime - startTime
    print('over! It downloads ' + str(runTime))
    time.sleep(1)
# 将请求失败的数据再次下载
for i in range(0,len(err)):
    print('Start! Ready to count!')
    startTime = time.time()
    url = err[i]
    try:
        print('Trying to connect!')
        res = requests.get(url).content
    except:
        err_txt.append(url)
        print('error! To be a txt!')
        continue
    outpath = time.strftime("%Y%m%d%H%M%S", time.localtime())
    with open('C:/Users/木鲸/OneDrive/setup/'+(outpath)+".jpg","wb") as file:
        file.write(res)
    endTime = time.time()
    runTime = endTime - startTime
    print('over! It downloads ' + str(runTime))
    time.sleep(1)
# 将最终错误的url写入一个存放错误的txt中
with open ('C:/Users/木鲸/OneDrive/err_txt.txt','a') as f:
    for i in range(0,len(err_txt)):
        f.write(err_txt[i]+'\n')
# 结束!
print('All over!')

下载err_txt.txt中的图片内容

import requests
import time

data = []
err = []

with open ('C:/Users/木鲸/OneDrive/err_txt.txt','r') as f:
    data = f.read().splitlines()

for i in range(0,len(data)):
    print('Start! Ready to count!')
    startTime = time.time()
    url = data[i]
    try:
        print('Trying to connect!')
        res = requests.get(url).content
    except:
        err.append(url)
        print('error!')
        continue
    outpath = time.strftime("%Y%m%d%H%M%S", time.localtime())
    with open('C:/Users/木鲸/OneDrive/setup/'+outpath+".jpg",'wb') as file:
        file.write(res)
    endTime = time.time()
    runTime = endTime - startTime
    print('Over! It downloads ' + str(runTime))
    time.sleep(1)

for i in range(0,len(err)):
    print('Start! Ready to count!')
    startTime = time.time()
    url = err[i]
    try:
        print("Trying to connect error_txt!")
        res = requests.get(url).content
    except:
        print('Error! Try again!')
        res = requests.get(url).content
    else:
        pass
    outpath = time.strftime("%Y%m%d%H%M%S", time.localtime())
    with open('C:/Users/木鲸/OneDrive/setup/'+outpath+".jpg",'wb') as file:
        file.write(res)
    endTime = time.time()
    runTime = endTime - startTime
    print('Over! It downloads ' + str(runTime))
    time.sleep(1)

print('All over!')

这个版本虽然解决了下载失败的部分问题,但他的缺点也是显而易见的,速度太慢了。一张图快的时候3秒下载完成,慢的时候需要200秒。这种单线程下载实在是太慢了。

所以我决定这个时候去学习多线程下载!

Version 03

这是第一个多线程版本,也是废了半天时间写出来的。

由于那个时候api接口是有连接限制,例如不能过快访问,不然ip会被ban,而且每天访问的次数只有300次。

所以我写了一个多线程脚本,大致思想就是,进行20次循环,每次循环中访问API 15次,并且开15个线程进行图片下载。等这这一轮的线程结束,进行下一次循环。

这个版本的脚本不仅有记录err的txt,也有多线程的快速,可以说是在API没有无限开放时候的巅峰了。

脚本如下:

import requests
import time
import threading
import re

url = 'https://api.lolicon.app/setu/?r18=2'
headers = {
    'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36 Edg/91.0.864.41'
}

def download(url,pid,way):
    try:
        res = requests.get(url,headers=headers).content
    except:
        print('Download error!')
        with open ('C:/Users/木鲸/OneDrive/setup/err.txt',"a") as file :
            file.write(setu_url+'\n')
        return
    with open('C:/Users/木鲸/OneDrive/setup/'+ str(pid)+ way, "wb") as file:
        file.write(res)

for count in range(1,21):
    print(f'开始第{count}次下载!')
    setu_urls = []
    setu_pids = []
    ways = []
    threads = []
    for i in range(1,16):
        try:
            print('Try to connect api!')
            res = requests.get(url,headers=headers)
            print('Successful connection!')
            try:
                data = res.json()
                data = data['data'][0]
                setu_url = data['url']
                setu_pid = data['pid']
            except:
                print('JOSN error!')
                continue
            way = re.findall('(.png|.jpg)', setu_url,re.S)[0]
            ways.append(way)
            setu_urls.append(setu_url)
            setu_pids.append(setu_pid)
            time.sleep(0.5)
        except:
            print('Connect api error!')
            time.sleep(1)
            continue
    for i in range(len(setu_urls)):
        t = threading.Thread(target=download, args=(setu_urls[i],setu_pids[i],ways[i],))
        t.start()
        threads.append(t)
    for t in threads:
        t.join()
        print(f'{t.name}-执行下载!')

Version 04

这个版本是最终版本。

今天早上看到室友写的一份脚本,发现同样的api接口多了一个num参数,一次访问可以得到最多100份数据。

再上API网站,发现开放限制了。

于是直接写了一份不需要等待,一次爬100张图片的最终版本。

脚本如下:

import requests
import re
import threading
import time

# 获取json数据
def getApiJSON(url,headers):
    try:
        print('Try to connect api!')
        res = requests.get(url,headers=headers)
        print('Successful connecting!')
    except:
        print('Connect error!')
        return getApiJSON(url,headers)
    JSON_data = res.json()
    return JSON_data

# 进行图片下载
def download(url, pid, way):
    global errCount
    try:
        res = requests.get(url).content
    except:
        print('Download error!')
        lock.acquire()
        errCount += 1
        lock.release()
        with open ('C:/Users/木鲸/OneDrive/setup/err.txt',"a") as file :
            file.write(setu_url+'\n')
        return
    with open('C:/Users/木鲸/OneDrive/setup/'+ pid + way, "wb") as file:
        file.write(res)
    print('Success!')

allTime = 0
allSuccess = 0

# 每次循环下载100张图
for i in range(2):
    print(f'The {i+1} time!')
    url = 'https://api.lolicon.app/setu/?num=100&r18=1'
    headers = {'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36 Edg/91.0.864.41'}
    lock = threading.Lock()
    threads = []
    errCount = 0
    startTime = time.time()
    jsons = getApiJSON(url, headers)
    jsons = jsons['data']

    for json in jsons:
        setu_url = json['url']
        setu_pid = str(json['pid'])
        way = re.findall('(.png|.jpg)', setu_url, re.S)[0]
        t = threading.Thread(target=download, args=(setu_url,setu_pid,way,))
        t.start()
        threads.append(t)
        time.sleep(1)

    for t in threads:
        t.join()
        print(f'{t.name} over!')

    endTime = time.time()
    print(f'The {i+1} time over! It takes {endTime-startTime}! It has {100-errCount} successes!')
    allTime += endTime - startTime
    allSuccess += 100 - errCount
    time.sleep(1)

print(f"All over! It takes {allTime}! It has {allSuccess} successes!")

最终测试发现。如果每次time.sleep(0.5),会有25%的错误率,速度也是在300s左右。但是晚上运行脚本的时候,无意间改成了time.sleep(1),发现错误率为0,并且速度也是在210s左右。几乎是1s一张图片。可以说在速度和错误率上都是最优的了。

后话

当然,本人也是刚刚学习的python多线程,这个最终版本的脚本也是花了一天时间不断的改出来的,所以可能并不是最完美的。但是在我这里,这个多线程setu爬取脚本已经毕业了。从最初的单线程下载,到后面的记录错误下载,再到后面的多线程爬取,最后的不断改进。

在这个过程中,我发现,枯燥的听网课是很难运用一个知识的。只有在学习的过程中去真正自己动手,这样知识才能学好。

所以在学习的时候,实践、实践、实践!这点很重要!

当然,看看这个文件夹的大小,你就知道这几天我有多努力了(bushi)

image-20210609001645336

从开始有多线程的想法(飞机师傅的指导)

image-20210609002326159

image-20210609002231335

到学习多线程遇到瓶颈

image-20210609002610366

image-20210609001827101

再到最后发现更改睡眠时间的欣喜若狂

image-20210609001932118

其实这样学习才是快乐的~