作者: 孔扎根
簡介: 工作13余載,現任高級爬蟲工程師,在工作中積累了豐富的數據庫、ETL及python開發方面的經驗。
座佑銘:美的東西都喜歡, 好的東西都想要, 美好的東西是我追求的目標
Python是一種跨平台的計算機編程語言。是一種面向對象的動態類型語言,Python的設計哲學是“優雅”、“明確”、“簡單”,隨着版本的不斷更新和語言新功能的添加,越多被用於獨立的、大型項目的開發。
本篇文章產生的背景是源於公司的一個項目"數字大屏",從公司的服務器下載圖片后, 對圖像進行處理, 本篇記錄了其中的一個小知識點, 在極端網絡波動下的圖片智能下載處理,我們知道網絡穩定順暢時一切下載都很順利, 程序結束的很完美,你開心我也開心^_^哈哈。但是圖片往往都比較大,當偶爾網絡極其慢的時候下載一張圖片可能需要很久的時間,而此時業務還要求你不能等太長時間, 怎么辦呢? 此時我們可能就希望你不要再下載了, 中斷吧……, 我再想想其它法…^_^.
別瞎叨叨了, 說正事...
在使用python3下載圖片時, 常用的方法有urlretrieve和requests兩種, 但是不管哪種方法在網速極慢的情況下, 都會出現圖片下載卡住現象。那如何解決呢? 小編總結了幾種常用的圖片下載方法及中斷下載的方法, 記錄了一段尋找最佳方法的心路歷程。
方法一、socket: 設置默認超時
正常網速時下載情況是什么樣子呢?
#!/usr/bin/env python3 # -*- coding:utf-8 -*- # __author__:kzg import datetime import os from urllib.request import urlretrieve
# 函數運行計時器 def count_time(func): def int_time(*args, **kwargs): start_time = datetime.datetime.now() # 程序開始時間 func(*args, **kwargs) over_time = datetime.datetime.now() # 程序結束時間 total_time = (over_time-start_time).total_seconds() print("download picture used time %s seconds" % total_time) return int_time
# 下載回調函數, 計算下載進度 def callbackinfo(down, block, size): ''' 回調函數: down:已經下載的數據塊 block:數據塊的大小 size:遠程文件的大小 ''' per = 100.0 * (down * block) / size if per > 100: per = 100 print('%.2f%%' % per)
# 圖片下載 @count_time def downpic(url, filename): try: print("Start Down %s" % url) if os.path.exists(filename): os.remove(filename) urlretrieve(url, filename, callbackinfo) except socket.timeout as ex: print(ex) url = "http://p1.meituan.net/wedding/d4c375416ab70b8b0bc4247875c929b0434138.jpg%40800w_600h_0e_1l%7Cwatermark%3D1%26%26r%3D1%26p%3D9%26x%3D2%26y%3D2%26relative%3D1%26o%3D20" filename = r'C:\blueprint-face-detection\scripts\mv.jpg' downpic(url, filename) 結果: Start Down http://p1.meituan.net/wedding/d4c375416ab70b8b0bc4247875c929b0434138.jpg%40800w_600h_0e_1l%7Cwatermark%3D1%26%26r%3D1%26p%3D9%26x%3D2%26y%3D2%26relative%3D1%26o%3D20 0.00% 7.01% 14.02% 21.03% 28.04% 35.05% 42.06% 49.07% 56.08% 63.09% 70.10% 77.11% 84.12% 91.13% 98.14% 100.00% download picture used time 0.165558 seconds
結論: 網速較快時下載完全沒問題, 僅用零點幾秒。
如果把圖片下載速度降低, 那下載完圖片的整體時間會拉長, 如果網速特別低,那下載完圖片的時候會無限延長。
那試試在限制網速的情況下, 下載圖片會出現什么現象呢, 如結論所說的那樣嗎?
2、使用charles限制網速, 設置為下載1k/s, 上傳10k/s
1、設置一下charles吧
2、設置socket的默認超時時間為10秒
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
# __author__:kzg
import datetime
import os
import socket
from urllib.request import urlretrieve
# 函數運行計時器
def count_time(func):
def int_time(*args, **kwargs):
start_time = datetime.datetime.now() # 程序開始時間
func(*args, **kwargs)
over_time = datetime.datetime.now() # 程序結束時間
total_time = (over_time-start_time).total_seconds()
print("download picture used time %s seconds" % total_time)
return int_time
# 下載回調函數, 計算下載進度
def callbackinfo(down, block, size):
'''
回調函數:
down:已經下載的數據塊
block:數據塊的大小
size:遠程文件的大小
'''
per = 100.0 * (down * block) / size
if per > 100:
per = 100
print('%.2f%%' % per)
# 圖片下載
@count_time
def downpic(url, filename):
try:
print("Start Down %s" % url)
if os.path.exists(filename):
os.remove(filename)
urlretrieve(url, filename, callbackinfo)
except socket.timeout as ex:
print(ex)
url = "http://p1.meituan.net/wedding/d4c375416ab70b8b0bc4247875c929b0434138.jpg%40800w_600h_0e_1l%7Cwatermark%3D1%26%26r%3D1%26p%3D9%26x%3D2%26y%3D2%26relative%3D1%26o%3D20"
filename = r'C:\blueprint-face-detection\scripts\mv.jpg'
# 設置10秒超時
socket.setdefaulttimeout(10)
downpic(url, filename)
結果: Start Down http://p1.meituan.net/wedding/d4c375416ab70b8b0bc4247875c929b0434138.jpg%40800w_600h_0e_1l%7Cwatermark%3D1%26%26r%3D1%26p%3D9%26x%3D2%26y%3D2%26relative%3D1%26o%3D20 timed out download picture used time 10.013625 seconds
結論: 正如期望的那樣, 10秒種后socket超時報出錯誤,urlretrieve下載過程被迫中斷,進而捕獲錯誤信息打印出來。
又重試了3次, 每次都好使, 好高興 ^_^
那再猜想一下, 如果把網速提高一些呢, 應該更沒問題了!!!, 下載速度設置為10k/s, 上傳10k/s 。
看圖:
運行一下:
完啦, 擦擦擦, 這是什么情況, 網速提高了, 10秒鍾早過了, 按說下載應該結束才對, 這都過半分鍾了咋還在運行呢?
經過多次嘗試發現下載速度超過4k/s時, socket設置defaulttime好像就不起作用了, 圖片下載不能被中斷, 由此可見想用此方法在任何情況下完全做到強制中斷圖片下載是不行的。
那咱辦呀,要不要在一顆樹上吊死呀,那就個換個方法試試唄。
方法二、線程
一說到線程, 想到最多的可能就是父子線程了,主線程結束子線程也跟着結束,聽着感覺很完美, 這個咱也會呀,啥不說了, 上代碼看看效果吧。
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
# __author__:kzg
import threading
import os, datetime, time
from urllib.request import urlretrieve
# 回調函數
def callbackinfo(down, block, size):
'''
回調函數:
down:已經下載的數據塊
block:數據塊的大小
size:遠程文件的大小
'''
per = 100.0 * (down * block) / size
if per > 100:
per = 100
print('%.2f%%' % per)
# 計時器
def count_time(func):
def int_time(*args, **kwargs):
start_time = datetime.datetime.now() # 程序開始時間
func(*args, **kwargs)
over_time = datetime.datetime.now() # 程序結束時間
total_time = (over_time-start_time).total_seconds()
print("download picture used time %s seconds" % total_time)
return int_time
# 圖片下載函數
@count_time
def downpic(url):
print("Start Down %s" % url)
if os.path.exists(filename):
os.remove(filename)
urlretrieve(url, filename, callbackinfo)
# 子線程
def mainFunc(funcname, args):
t = threading.Thread(target=funcname, args=(args,))
t.start()
t.join(10) # 子線程阻塞10秒
url = "http://p1.meituan.net/wedding/d4c375416ab70b8b0bc4247875c929b0434138.jpg%40800w_600h_0e_1l%7Cwatermark%3D1%26%26r%3D1%26p%3D9%26x%3D2%26y%3D2%26relative%3D1%26o%3D20"
filename = r'C:\blueprint-face-detection\scripts\mv.jpg'
# 主線程
m = threading.Thread(target=mainFunc, args=(downpic, url))
m.setDaemon(True)
m.start()
m.join() # 10秒后取消子線程阻塞, 主線程不阻塞立即運行后面程序
print("m is alive? ", m.is_alive())
time.sleep(10)
os.remove(filename)
將下載速度分別限制在1k/st和10k/s,結果如下
結論: 主線程已經結束, 但子線程並未結束, 圖片下載在持續中, 直到整個程序結束后, 下載被中斷。所以通過父線程結束子線程也結束的方法來強制圖片中斷下載是不可行的。
這下可慘啦, 沒辦法實現了嗎? 於是乎在網上找呀找, 終於看到有人也遇到了這類問題, 怎么解決呢? 。。。。 強制中斷線程, 聽起來很霸氣,那咱也跟着試試吧, 上代碼。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- # __author__:kzg import threading import os, datetime, time import ctypes import inspect from urllib.request import urlretrieve # 回調函數 def callbackinfo(down, block, size): ''' 回調函數: down:已經下載的數據塊 block:數據塊的大小 size:遠程文件的大小 ''' per = 100.0 * (down * block) / size if per > 100: per = 100 print('%.2f%%' % per) # 計時器 def count_time(func): def int_time(*args, **kwargs): start_time = datetime.datetime.now() # 程序開始時間 func(*args, **kwargs) over_time = datetime.datetime.now() # 程序結束時間 total_time = (over_time-start_time).total_seconds() print("download picture used time %s seconds" % total_time) return int_time # 圖片下載函數 @count_time def downpic(url): print("Start Down %s" % url) if os.path.exists(filename): os.remove(filename) urlretrieve(url, filename, callbackinfo) def _async_raise(tid, exctype): """raises the exception, performs cleanup if needed""" tid = ctypes.c_long(tid) if not inspect.isclass(exctype): exctype = type(exctype) res = ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, ctypes.py_object(exctype)) if res == 0: raise ValueError("invalid thread id") elif res != 1: # """if it returns a number greater than one, you're in trouble, # and you should call it again with exc=NULL to revert the effect""" ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, None) raise SystemError("PyThreadState_SetAsyncExc failed") def stop_thread(thread): _async_raise(thread.ident, SystemExit) url = "http://p1.meituan.net/wedding/d4c375416ab70b8b0bc4247875c929b0434138.jpg%40800w_600h_0e_1l%7Cwatermark%3D1%26%26r%3D1%26p%3D9%26x%3D2%26y%3D2%26relative%3D1%26o%3D20" filename = r'C:\blueprint-face-detection\scripts\mv.jpg' # 開啟線程下載圖片 t = threading.Thread(target=downpic, args=(url,)) t.start() t.join(10) # 強制結束線程 if t.is_alive(): stop_thread(t) time.sleep(3) print("thread is alive?", t.is_alive()) os.remove(filename)
結論:
下載速度設置為1k/s, 8k/s, 10k/s, 15k/s時, 結果如下,跟預期結果一致(重試幾次結果一致)
下載速度設置為3k/s,12k/s時結果如下(重試幾次結果一致)
似乎看出點什么了吧, 為什么同一個程序, 不同網速下執行結果不一致呢?因實現網速我們是無法控制的,所以此方法大部分情況下都是可以的, 但也會出現例外。
python下載圖片時, 每次下載8k, 下載完就會寫入一次文件。
方法三、 request
timeout 僅對連接過程有效,與響應體的下載無關。 timeout 並不是整個下載響應的時間限制,而是如果服務器在 timeout 秒內沒有應答,將會引發一個異常(更精確地說,是在 timeout 秒內沒有從基礎套接字上接收到任何字節的數據)。
換句話說: 如果在timeout時間內沒有返回任何字節數據, 則request會報錯退出, 如果在timeout時間內收到了任何字節的數據, 則request會一直下載, 直到響應體被下載完。
1、正常網速下的現象
# -*- coding: utf-8 -*-
import requests
import datetime
url = "http://p1.meituan.net/wedding/d4c375416ab70b8b0bc4247875c929b0434138.jpg%40800w_600h_0e_1l%7Cwatermark%3D1%26%26r%3D1%26p%3D9%26x%3D2%26y%3D2%26relative%3D1%26o%3D20"
filename = r'C:\blueprint-face-detection\scripts\meinv.jpg'
# 計時器
def count_time(func):
def int_time(*args, **kwargs):
start_time = datetime.datetime.now() # 程序開始時間
func(*args, **kwargs)
over_time = datetime.datetime.now() # 程序結束時間
total_time = (over_time-start_time).total_seconds()
print("download picture used time %s seconds" % total_time)
return int_time
@count_time
def downpic(url):
ret = requests.get(url)
print("req status is: ", ret.status_code)
with open(filename, 'wb') as fp:
print("start write picture..")
fp.write(ret.content)
downpic(url)
結果:
req status is: 200
start write picture..
download picture used time 0.26226 seconds
結論: 正常網速下, request請求成功了, 美女0.3秒被成功請出。
1、將網速限制在100k/s,結果如何呢 ?
結論: 下載速度為100k/s時, request也成功了, 美女經過15.6秒被成功請出來。
2. 將網速限制在50k/s,結果如何呢?
結論: 下載速度為50k/s時, request也成功了, 美女經過40.9秒被成功請出來。 設置timeout=3, 是否會生效呢?
感覺要絕望了,咋整呀? 沒有一種方法可以及時中斷下載嗎?經過不斷的嘗試終於找到了一個可行的方法。
函數中斷 !!! 如果超過設定時間則正在下載的函數被中斷, 試一下吧。
# #!/usr/bin/python3 # # -*- coding: utf-8 -*- import requests import datetime from func_timeout import func_set_timeout, FunctionTimedOut url = "http://pic1.win4000.com/wallpaper/5/58b3b56d0a0a7.jpg" @func_set_timeout(10) def downpic(url): ret = requests.get(url) print("Start Down %s" % url) if ret.status_code == 200: with open('meinv.jpg', 'wb') as fp: fp.write(ret.content) print("downLoad pic ok %s" % url) print(datetime.datetime.now()) downpic(url) print(datetime.datetime.now())
運行一下程序, 正常網速時下載一張圖片的時間是多少 !
可以看出大概8秒時間可以下載完一張圖片, 那我將時間設置為5秒, 那圖片下載會被中斷,肯定下不完。
正如預料的那樣, 5秒鍾后圖片下載被中斷了, 經過多次檢查, 圖片並沒有繼續下載, 證明圖片下載被中斷成功。
哈……