我如何加快在python中使用urllib2获取页面?

我有一个脚本,提取几个网页,并parsing信息。

(可以在http://bluedevilbooks.com/search/?DEPT=MATH&CLASS=103&SEC=01上看到一个例子)

我跑了cProfile,正如我所设想的,urlopen占用了很多时间。 有没有办法更快地获取页面? 或者一次获取多个页面的方式? 我会做任何最简单的,因为我是python和web开发的新手。

提前致谢! 🙂

更新:我有一个函数称为fetchURLs() ,我用它来创build一个我需要的URL的数组,这样的东西就像urls = fetchURLS() 。这些URL都是来自Amazon和eBay API的XML文件(这使我困惑urls = fetchURLS()加载需要很长时间,也许我的虚拟主机速度很慢?)

我需要做的是加载每个URL,读取每个页面,并将数据发送到脚本的另一部分,将parsing和显示数据。

请注意,我不能做后面的部分,直到所有的页面被提取,这就是我的问题是。

此外,我的主机一次限制我25个进程,我相信,所以最简单的服务器上将是好的:)


这是时候了:

 Sun Aug 15 20:51:22 2010 prof 211352 function calls (209292 primitive calls) in 22.254 CPU seconds Ordered by: internal time List reduced from 404 to 10 due to restriction <10> ncalls tottime percall cumtime percall filename:lineno(function) 10 18.056 1.806 18.056 1.806 {_socket.getaddrinfo} 4991 2.730 0.001 2.730 0.001 {method 'recv' of '_socket.socket' objects} 10 0.490 0.049 0.490 0.049 {method 'connect' of '_socket.socket' objects} 2415 0.079 0.000 0.079 0.000 {method 'translate' of 'unicode' objects} 12 0.061 0.005 0.745 0.062 /usr/local/lib/python2.6/HTMLParser.py:132(goahead) 3428 0.060 0.000 0.202 0.000 /usr/local/lib/python2.6/site-packages/BeautifulSoup.py:1306(endData) 1698 0.055 0.000 0.068 0.000 /usr/local/lib/python2.6/site-packages/BeautifulSoup.py:1351(_smartPop) 4125 0.053 0.000 0.056 0.000 /usr/local/lib/python2.6/site-packages/BeautifulSoup.py:118(setup) 1698 0.042 0.000 0.358 0.000 /usr/local/lib/python2.6/HTMLParser.py:224(parse_starttag) 1698 0.042 0.000 0.275 0.000 /usr/local/lib/python2.6/site-packages/BeautifulSoup.py:1397(unknown_starttag) 

编辑 :我正在扩大答案,包括更精美的例子。 在这篇文章中,我发现了很多敌意和错误信息,关于线程和asynchronousI / O。 所以我也加了更多的论据来驳斥某些无效的主张。 我希望这能帮助人们为正确的工作select正确的工具。

这是3天前的问题。

Python的urllib2.open是慢的,需要一个更好的方式来阅读几个url – Python溢出Python urllib2.urlopen()是缓慢的,需要一个更好的方式来阅读几个url

我正在抛光代码,以显示如何使用线程并行获取多个网页。

 import time import threading import Queue # utility - spawn a thread to execute target for each args def run_parallel_in_threads(target, args_list): result = Queue.Queue() # wrapper to collect return value in a Queue def task_wrapper(*args): result.put(target(*args)) threads = [threading.Thread(target=task_wrapper, args=args) for args in args_list] for t in threads: t.start() for t in threads: t.join() return result def dummy_task(n): for i in xrange(n): time.sleep(0.1) return n # below is the application code urls = [ ('http://www.google.com/',), ('http://www.lycos.com/',), ('http://www.bing.com/',), ('http://www.altavista.com/',), ('http://achewood.com/',), ] def fetch(url): return urllib2.urlopen(url).read() run_parallel_in_threads(fetch, urls) 

正如你所看到的,应用程序特定的代码只有3行,如果你是积极的,可以折叠成1行。 我认为任何人都不能certificate他们认为这是复杂的,不可维护的。

不幸的是,这里发布的大多数其他线程代码都有一些缺陷。 他们中的许多人进行活动轮询等待代码完成。 join()是同步代码的更好方法。 到目前为止,我认为这个代码已经改进了所有的线程示例。

保持连接

如果你所有的URL指向同一个服务器,WoLpH关于使用保持连接的build议可能会非常有用。

扭曲

亚伦Gallagher是一个twisted框架的球迷,他是任何build议线程的敌意。 不幸的是,他的很多说法都是错误的信息。 例如,他说:“-1表示build议的线程,这是IO绑定的;线程在这里是无用的。 这与Nick T和我已经certificate使用线程获得速度的证据相反。 实际上,I / O绑定应用程序是使用Python线程获得最多的(而不是在CPU绑定应用程序中获得的)。 亚伦对线索的误导性批评表明,他对平行编程总体上感到困惑。

正确的工作的正确工具

我很清楚与使用线程,python,asynchronousI / O等的并行编程有关的问题。 每个工具都有其优点和缺点。 对于每种情况都有一个合适的工具。 我不反对扭曲(虽然我没有自己部署)。 但我不相信我们可以说,在所有情况下线程都是坏的,并且扭曲是好的。

例如,如果OP的要求是并行访问10,000个网站,则asynchronousI / O将是优先的。 线程不会是可用的(除非可能用无堆栈的Python)。

亚伦对线索的反对大多是泛泛之谈。 他没有认识到这是一个微不足道的平行任务。 每个任务是独立的,不共享资源。 所以他的大部分攻击都不适用。

鉴于我的代码没有外部依赖,我会把它称为正确的工具正确的工作。

性能

我想大多数人会同意,这个任务的执行主要取决于networking代码和外部服务器,平台代码的性能应该可以忽略不计。 不过,Aaron的基准testing显示,线程代码的速度提高了50%。 我认为有必要回应这个明显的速度增益。

在Nick的代码中,有一个明显的缺陷导致效率低下。 但是,你如何解释我的代码233毫秒的速度增益? 我认为,即使扭曲的球迷也不会因为扭曲的效率而得出结论。 毕竟,在系统代码之外还有大量的variables,比如远程服务器的性能,networking,caching以及urllib2和扭曲的web客户端之间的差异等等。

为了确保Python的线程不会导致大量的低效率,我做了一个快速的基准来产生5个线程,然后500个线程。 我可以很舒服的说产卵5线程的开销是微不足道的,不能解释233ms的速度差异。

 In [274]: %time run_parallel_in_threads(dummy_task, [(0,)]*5) CPU times: user 0.00 s, sys: 0.00 s, total: 0.00 s Wall time: 0.00 s Out[275]: <Queue.Queue instance at 0x038B2878> In [276]: %time run_parallel_in_threads(dummy_task, [(0,)]*500) CPU times: user 0.16 s, sys: 0.00 s, total: 0.16 s Wall time: 0.16 s In [278]: %time run_parallel_in_threads(dummy_task, [(10,)]*500) CPU times: user 1.13 s, sys: 0.00 s, total: 1.13 s Wall time: 1.13 s <<<<<<<< This means 0.13s of overhead 

对我的并行读取进一步testing显示,在17次运行中,响应时间变化很大。 (不幸的是我没有扭曲来validationAaron的代码)。

 0.75 s 0.38 s 0.59 s 0.38 s 0.62 s 1.50 s 0.49 s 0.36 s 0.95 s 0.43 s 0.61 s 0.81 s 0.46 s 1.21 s 2.87 s 1.04 s 1.72 s 

我的testing不支持Aaron的结论,即线程一直比asynchronousI / O慢一点可测量的边界。 鉴于所涉及的variables数量,我不得不说这不是测量asynchronousI / O和线程之间的系统性能差异的有效testing。

使用扭曲 ! 与使用线程相比,它使这种事情变得很荒唐。

 from twisted.internet import defer, reactor from twisted.web.client import getPage import time def processPage(page, url): # do somewthing here. return url, len(page) def printResults(result): for success, value in result: if success: print 'Success:', value else: print 'Failure:', value.getErrorMessage() def printDelta(_, start): delta = time.time() - start print 'ran in %0.3fs' % (delta,) return delta urls = [ 'http://www.google.com/', 'http://www.lycos.com/', 'http://www.bing.com/', 'http://www.altavista.com/', 'http://achewood.com/', ] def fetchURLs(): callbacks = [] for url in urls: d = getPage(url) d.addCallback(processPage, url) callbacks.append(d) callbacks = defer.DeferredList(callbacks) callbacks.addCallback(printResults) return callbacks @defer.inlineCallbacks def main(): times = [] for x in xrange(5): d = fetchURLs() d.addCallback(printDelta, time.time()) times.append((yield d)) print 'avg time: %0.3fs' % (sum(times) / len(times),) reactor.callWhenRunning(main) reactor.run() 

这个代码也比任何其他的解决scheme都更好(我在closures了一些使用大量带宽的东西之后进行了编辑):

 Success: ('http://www.google.com/', 8135) Success: ('http://www.lycos.com/', 29996) Success: ('http://www.bing.com/', 28611) Success: ('http://www.altavista.com/', 8378) Success: ('http://achewood.com/', 15043) ran in 0.518s Success: ('http://www.google.com/', 8135) Success: ('http://www.lycos.com/', 30349) Success: ('http://www.bing.com/', 28611) Success: ('http://www.altavista.com/', 8378) Success: ('http://achewood.com/', 15043) ran in 0.461s Success: ('http://www.google.com/', 8135) Success: ('http://www.lycos.com/', 30033) Success: ('http://www.bing.com/', 28611) Success: ('http://www.altavista.com/', 8378) Success: ('http://achewood.com/', 15043) ran in 0.435s Success: ('http://www.google.com/', 8117) Success: ('http://www.lycos.com/', 30349) Success: ('http://www.bing.com/', 28611) Success: ('http://www.altavista.com/', 8378) Success: ('http://achewood.com/', 15043) ran in 0.449s Success: ('http://www.google.com/', 8135) Success: ('http://www.lycos.com/', 30349) Success: ('http://www.bing.com/', 28611) Success: ('http://www.altavista.com/', 8378) Success: ('http://achewood.com/', 15043) ran in 0.547s avg time: 0.482s 

并且使用Nick T的代码,装备也给了五个的平均和更好显示输出:

 Starting threaded reads: ...took 1.921520 seconds ([8117, 30070, 15043, 8386, 28611]) Starting threaded reads: ...took 1.779461 seconds ([8135, 15043, 8386, 30349, 28611]) Starting threaded reads: ...took 1.756968 seconds ([8135, 8386, 15043, 30349, 28611]) Starting threaded reads: ...took 1.762956 seconds ([8386, 8135, 15043, 29996, 28611]) Starting threaded reads: ...took 1.654377 seconds ([8117, 30349, 15043, 8386, 28611]) avg time: 1.775s Starting sequential reads: ...took 1.389803 seconds ([8135, 30147, 28611, 8386, 15043]) Starting sequential reads: ...took 1.457451 seconds ([8135, 30051, 28611, 8386, 15043]) Starting sequential reads: ...took 1.432214 seconds ([8135, 29996, 28611, 8386, 15043]) Starting sequential reads: ...took 1.447866 seconds ([8117, 30028, 28611, 8386, 15043]) Starting sequential reads: ...took 1.468946 seconds ([8153, 30051, 28611, 8386, 15043]) avg time: 1.439s 

用伟业堂的代号:

 Fetched 8117 from http://www.google.com/ Fetched 28611 from http://www.bing.com/ Fetched 8386 from http://www.altavista.com/ Fetched 30051 from http://www.lycos.com/ Fetched 15043 from http://achewood.com/ done in 0.704s Fetched 8117 from http://www.google.com/ Fetched 28611 from http://www.bing.com/ Fetched 8386 from http://www.altavista.com/ Fetched 30114 from http://www.lycos.com/ Fetched 15043 from http://achewood.com/ done in 0.845s Fetched 8153 from http://www.google.com/ Fetched 28611 from http://www.bing.com/ Fetched 8386 from http://www.altavista.com/ Fetched 30070 from http://www.lycos.com/ Fetched 15043 from http://achewood.com/ done in 0.689s Fetched 8117 from http://www.google.com/ Fetched 28611 from http://www.bing.com/ Fetched 8386 from http://www.altavista.com/ Fetched 30114 from http://www.lycos.com/ Fetched 15043 from http://achewood.com/ done in 0.647s Fetched 8135 from http://www.google.com/ Fetched 28611 from http://www.bing.com/ Fetched 8386 from http://www.altavista.com/ Fetched 30349 from http://www.lycos.com/ Fetched 15043 from http://achewood.com/ done in 0.693s avg time: 0.715s 

我必须说,我喜欢顺序提取对我更好

这里是一个使用python Threads的例子。 其他线程示例在这里为每个url启动一个线程,如果服务器处理的命中太多,这不是非常友好的行为(例如,蜘蛛在同一个主机上有很多url)

 from threading import Thread from urllib2 import urlopen from time import time, sleep WORKERS=1 urls = ['http://docs.python.org/library/threading.html', 'http://docs.python.org/library/thread.html', 'http://docs.python.org/library/multiprocessing.html', 'http://docs.python.org/howto/urllib2.html']*10 results = [] class Worker(Thread): def run(self): while urls: url = urls.pop() results.append((url, urlopen(url).read())) start = time() threads = [Worker() for i in range(WORKERS)] any(t.start() for t in threads) while len(results)<40: sleep(0.1) print time()-start 

注意:这里给出的时间是40个URL,并且将取决于您的Internet连接速度和服务器的延迟。 在澳大利亚,我的平均时间> 300ms

WORKERS=1运行需要86秒
有了WORKERS=4 ,跑了23秒
WORKERS=10它花了10秒钟跑

所以有10个线程下载的速度是单线程的8.6倍。

这是使用队列的升级版本。 至less有几个优点。
1.请按照它们出现在列表中的顺序请求这些url
2.可以使用q.join()来检测请求全部完成
3.结果保持与url列表相同的顺序

 from threading import Thread from urllib2 import urlopen from time import time, sleep from Queue import Queue WORKERS=10 urls = ['http://docs.python.org/library/threading.html', 'http://docs.python.org/library/thread.html', 'http://docs.python.org/library/multiprocessing.html', 'http://docs.python.org/howto/urllib2.html']*10 results = [None]*len(urls) def worker(): while True: i, url = q.get() # print "requesting ", i, url # if you want to see what's going on results[i]=urlopen(url).read() q.task_done() start = time() q = Queue() for i in range(WORKERS): t=Thread(target=worker) t.daemon = True t.start() for i,url in enumerate(urls): q.put((i,url)) q.join() print time()-start 

大部分的答案都集中在同时从不同服务器获取多个页面(线程),而不是重用已经打开的HTTP连接。 如果OP向同一个服务器/站点发出多个请求。

在urlib2中,每个请求都会创build一个单独的连接,这会影响性能,并且导致访存页面的速度较慢。 urllib3通过使用连接池解决了这个问题。 可以在这里阅读更多urllib3 [也是线程安全的]

还有一个请求使用urllib3的HTTP库

这与线程结合应该会提高抓取页面的速度

实际的等待可能不在urllib2而是在服务器和/或到服务器的networking连接中。

有两种方法可以加快速度。

  1. 保持连接活着(请参阅以下问题: Python urllib2保持活动状态 )
  2. 使用多连接,你可以使用线程或asynchronous方法,如Aaron Gallagher所build议的。 为此,只需使用任何线程示例,并且您应该做的很好:)您也可以使用multiprocessing库使事情变得非常容易。

现在有一个很好的Python库,可以为你调用请求 。

如果你想要基于线程或asynchronousAPI的解决scheme(在底层使用gevent),如果你想要基于非阻塞IO的解决scheme,请使用标准的API请求。

由于这个问题已经发布,看起来像有一个更高级别的抽象可用, ThreadPoolExecutor

https://docs.python.org/3/library/concurrent.futures.html#threadpoolexecutor-example

为了方便起见,在这里粘贴的例子是:

 import concurrent.futures import urllib.request URLS = ['http://www.foxnews.com/', 'http://www.cnn.com/', 'http://europe.wsj.com/', 'http://www.bbc.co.uk/', 'http://some-made-up-domain.com/'] # Retrieve a single page and report the url and contents def load_url(url, timeout): with urllib.request.urlopen(url, timeout=timeout) as conn: return conn.read() # We can use a with statement to ensure threads are cleaned up promptly with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: # Start the load operations and mark each future with its URL future_to_url = {executor.submit(load_url, url, 60): url for url in URLS} for future in concurrent.futures.as_completed(future_to_url): url = future_to_url[future] try: data = future.result() except Exception as exc: print('%r generated an exception: %s' % (url, exc)) else: print('%r page is %d bytes' % (url, len(data))) 

还有我认为使代码更容易的map : https : //docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor.map

取得网页显然需要一段时间,因为你没有访问本地任何东西。 如果你有几个访问,你可以使用threading模块一次运行一对夫妇。

这是一个非常粗糙的例子

 import threading import urllib2 import time urls = ['http://docs.python.org/library/threading.html', 'http://docs.python.org/library/thread.html', 'http://docs.python.org/library/multiprocessing.html', 'http://docs.python.org/howto/urllib2.html'] data1 = [] data2 = [] class PageFetch(threading.Thread): def __init__(self, url, datadump): self.url = url self.datadump = datadump threading.Thread.__init__(self) def run(self): page = urllib2.urlopen(self.url) self.datadump.append(page.read()) # don't do it like this. print "Starting threaded reads:" start = time.clock() for url in urls: PageFetch(url, data2).start() while len(data2) < len(urls): pass # don't do this either. print "...took %f seconds" % (time.clock() - start) print "Starting sequential reads:" start = time.clock() for url in urls: page = urllib2.urlopen(url) data1.append(page.read()) print "...took %f seconds" % (time.clock() - start) for i,x in enumerate(data1): print len(data1[i]), len(data2[i]) 

这是我运行时的输出:

 Starting threaded reads: ...took 2.035579 seconds Starting sequential reads: ...took 4.307102 seconds 73127 19923 19923 59366 361483 73127 59366 361483 

通过附加到列表来抓取线程中的数据可能是不明智的(队列会更好),但它说明了有所不同。

这是一个标准的库解决scheme。 虽然速度并不快,但比使用线程解决scheme的内存less。

 try: from http.client import HTTPConnection, HTTPSConnection except ImportError: from httplib import HTTPConnection, HTTPSConnection connections = [] results = [] for url in urls: scheme, _, host, path = url.split('/', 3) h = (HTTPConnection if scheme == 'http:' else HTTPSConnection)(host) h.request('GET', '/' + path) connections.append(h) for h in connections: results.append(h.getresponse().read()) 

另外,如果你的大部分请求都在同一个主机上,那么重复使用相同的http连接可能不仅仅是并行处理。

请查找Pythonnetworking基准脚本,了解单连接慢度识别:

 """Python network test.""" from socket import create_connection from time import time try: from urllib2 import urlopen except ImportError: from urllib.request import urlopen TIC = time() create_connection(('216.58.194.174', 80)) print('Duration socket IP connection (s): {:.2f}'.format(time() - TIC)) TIC = time() create_connection(('google.com', 80)) print('Duration socket DNS connection (s): {:.2f}'.format(time() - TIC)) TIC = time() urlopen('http://216.58.194.174') print('Duration urlopen IP connection (s): {:.2f}'.format(time() - TIC)) TIC = time() urlopen('http://google.com') print('Duration urlopen DNS connection (s): {:.2f}'.format(time() - TIC)) 

Python 3.6的结果示例:

 Duration socket IP connection (s): 0.02 Duration socket DNS connection (s): 75.51 Duration urlopen IP connection (s): 75.88 Duration urlopen DNS connection (s): 151.42 

Python 2.7.13的结果非常相似。

在这种情况下,DNS和urlopen缓慢很容易识别。