2014-01-15 96 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))