2014-01-15 98 views
3

我正在製作一個程序下載一個大文件,並且我添加了一個功能,程序確定下載了多少百分比,並在每次下載另一個10%的時間並在什麼時間通知用戶(即print (str(percent) + " downloaded at " + str(time)))然而,當我在小文件上測試程序時,我注意到它的準確性不夠準確。這裏是我做的一個示例程序:Urllib進度不準確?

import urllib.request 

def printout(a, b, c): 
    print(str(a) + ", " + str(b) + ", " + str(c)) 

urllib.request.urlretrieve("http://downloadcenter.mcafee.com/products/tools/foundstone/fport.zip", r"C:\Users\Username\Downloads\fport.zip", reporthook = printout) 

這下載Fport,我正要下載的工具。無論如何,我得到這個輸出:

0, 8192, 57843 
1, 8192, 57843 
2, 8192, 57843 
3, 8192, 57843 
4, 8192, 57843 
5, 8192, 57843 
6, 8192, 57843 
7, 8192, 57843 
8, 8192, 57843 

我認爲這正是我想要的。當我注意到一個小錯誤時,我正準備放入。 8192不進入57843.不是8次。我把它插入計算器,發現它實際上大約有7次。考慮到這是一個相當大的差異。這種斷開連接影響更大的文件,但它仍然存在。這是一些元數據或頭?如果是這樣,它相當大,不是嗎?有沒有辦法解釋它(即它是否總是大約16000字節)?

+0

作爲一個邊請注意,您爲什麼首先使用傳統界面? – abarnert

+0

@abarnert不知道你的意思是傳統的界面......你的意思是'urllib'而不是'urllib2'? – KnightOfNi

+0

不,我的意思是'urllib.request.urlretrieve',它只記錄爲[legacy interface]的一部分(http://docs.python.org/3/library/urllib.request.html#legacy-interface) ,「在將來某個時候可能會被棄用」。 – abarnert

回答

1

所以,如果你看一下Lib/urllib/request.py(CPython的大約2.7)代碼,它變得很清楚,爲什麼是這樣的話:

with tfp: 
     result = filename, headers 
     bs = 1024*8 # we read 8KB at a time. 
     size = -1 
     read = 0 
     blocknum = 0 
     if "content-length" in headers: 
      size = int(headers["Content-Length"]) 

     if reporthook: 
      reporthook(blocknum, bs, size) 

     while True: 
      block = fp.read(bs) # here is where we do the read 
      if not block: 
       break 
      read += len(block) 
      tfp.write(block) 
      blocknum += 1 
      if reporthook: 
       reporthook(blocknum, bs, size) 

在最後一行,則reporthook被告知bs被讀取,不len(block) ,這可能會更準確。我不確定爲什麼會出現這種情況,即是否有充分的理由,或者是否是圖書館中的一個小錯誤。您可以向Python郵件程序詢問並/或提交一個錯誤。

注意:我認爲在固定大小的塊中讀取數據相當常見,例如參見fread。在那裏,如果遇到EOF(文件結束),返回值可能與請求讀取的字節數不同,這在Python read API中類似。

1

該文檔解釋說,reporthook每塊「塊」被調用一次,塊大小和總大小。

urllib.request不會嘗試使塊大小完全相等;它會嘗試使塊大小爲8192這樣的2,因爲這通常是最快和最簡單的。

所以,你想要做的是使用實際的字節來計算百分比,而不是塊數。


urlretrieve接口沒有給你一個簡單的方法來獲得實際的字節。如果您假設每個socket.recv(n)(但最後一個)實際返回n個字節,則不能保證計數塊。 os.stat(filename)只適用於(大多數平臺),如果您認爲urlretrieve在每次通話之前使用無緩衝文件或刷新,這再次不能保證。

這是不使用「傳統接口」的許多原因之一。

高層次的接口(只調用urllib.request.urlopen和使用Response作爲一個文件對象)可能看起來像它比urlretrieve提供的信息較少,但如果你讀urllib.request Restrictions,這使得它非常清楚,這是一種錯覺。因此,您可以使用urlopen,在這種情況下,您只需從一個文件對象複製到另一個文件對象,而不是使用有限的回調接口,因此您可以使用任何喜歡的文件對象複製功能,或者編寫自己的文件對象:

def copy(fin, fout, flen=None): 
    sofar = 0 
    while True: 
     buf = fin.read(8192) 
     if not buf: 
      break 
     sofar += len(buf) 
     if flen: 
      print('{}/{} bytes'.format(sofar, flen)) 
     fout.write(buf) 
    print('All done') 

r = urllib.request.urlopen(url) 
with open(path, 'wb') as f: 
    copy(r, f, r.headers.get('Content-Length')) 

如果你真的想要掛鉤到urllib的低級內容,那麼urlretrieve是不是這樣的東西;它只是假貨。你必須創建你自己的opener子類以及隨之而來的整個混亂。

如果你想這是幾乎一樣簡單urlopen但提供儘可能多的功能自定義揭幕戰的接口......嗯,urllib不具有,這就是爲什麼像存在requests第三方模塊。

+0

在這個問題的兩個答案之間,我知道爲什麼這是造成我的問題,一般如何解決它,但你可以更具體一點到我可能如何找到「實際字節」而不是塊數?謝謝 – KnightOfNi

+0

@ user2945577:給我一下,我會寫點東西。 – abarnert

+0

哇,這是很好的細節。我不認爲urlopen適合下載我的帖子中提到的「大文件」,所以我想這將是另一個模塊下載,一旦urlretrieve死去......感嘆。 – KnightOfNi

1

urllib.request的高級界面真的不適合你要做的事情。您可以使用較低級別的接口......但實際上,這是第三方庫requests使其級聯更簡單的一種方式。 (您不必使用requests -the各種curl包裝,例如,也使其比urllib容易。但是requests是最urllib樣和最簡單的第三方替代品。)

requests CAN像urllib一樣工作並自動將所有內容拉下來,但只需添加stream=True即可控制拉取數據。它有幾個不同的接口(解碼的Unicode行,字節行,原始數據從套接字等),但iter_content可能是你想要的一個 - 它可以按需提供大塊內容,適當緩衝,透明映射分塊轉移模式轉換爲平面轉移,處理100個繼續,...基本上HTTP可以向您發送的所有內容。所以:

with open(path, 'wb') as f: 
    r = requests.get(url, stream=True) 
    for chunk in r.iter_content(8192): 
     f.write(chunk) 

添加進度仍然需要手動完成。但是,由於您只是將大塊文件保存到背後的文件中,因此您確切知道您看過多少個字節。而且,只要服務器提供的Content-Length頭(其中一些服務器不會在某些情況下做的,但沒有什麼可以做,除了處理它),很容易:

with open(path, 'wb') as f: 
    r = requests.get(url, stream=True) 
    total = r.headers.get('content-length') 
    sofar = 0 
    for chunk in r.iter_content(8192): 
     f.write(chunk) 
     sofar += len(chunk) 
     if total: 
      print('{}/{}: {}%'.format(sofar, total, sofar*100.0/total)) 
     else: 
      print('{}/???: ???%'.format(sofar))