如何加速Python中使用urllib2获取页面的速度?

29 投票
11 回答
25963 浏览
提问于 2025-04-16 02:50

我有一个脚本,它会获取几个网页并解析里面的信息。

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

我用cProfile对它进行了分析,果然,urlopen这个函数花费了很多时间。有没有办法让网页加载得更快?或者能不能一次性获取多个网页?因为我刚开始学Python和网页开发,所以我希望能找到最简单的方法。

提前谢谢你们!:)

更新:我有一个叫做fetchURLs()的函数,用来生成我需要的URL数组,所以像这样urls = fetchURLs()。这些URL都是来自亚马逊和eBay的XML文件(这让我很困惑,为什么加载这么慢,也许是我的网络主机太慢了?)

我需要做的是加载每个URL,读取每个页面,然后把这些数据发送到脚本的另一部分去解析和显示。

需要注意的是,我不能在所有页面都加载完之前进行后面的操作,这就是我遇到的问题。

另外,我的主机限制我一次只能处理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)

11 个回答

5

这里有一个使用Python的Threads的例子。其他的线程示例是针对每个网址启动一个线程,这样做可能会对服务器造成很大的压力(比如,爬虫通常会在同一个主机上有很多网址)。

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个网址的,具体时间会受到你网络速度和到服务器延迟的影响。比如我在澳大利亚,延迟大于300毫秒。

WORKERS=1时,运行需要86秒
WORKERS=4时,运行需要23秒
WORKERS=10时,运行只需10秒

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

这里有一个升级版,使用了队列(Queue)。这样做有几个好处:
1. 请求的网址是按照列表中的顺序进行的
2. 可以使用q.join()来检测所有请求是否都完成
3. 结果的顺序和网址列表保持一致

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
19

使用twisted吧!跟用线程比起来,这种事情简单得多。

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()

这段代码的表现也比其他任何解决方案都要好(在我关闭了一些占用带宽的东西后进行了编辑):

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

还有使用Wai Yip Tung的代码:

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

我得说,我确实喜欢顺序获取数据的表现对我来说更好。

30

编辑: 我在扩展这个回答,加入了一个更完善的例子。我发现这个帖子里关于线程和异步输入输出的讨论有很多误解和敌意。因此,我也添加了一些论据来反驳某些不正确的说法。希望这能帮助大家选择合适的工具来完成合适的工作。

这是三天前一个问题的重复。

Python urllib2.open 速度慢,需要更好的方法来读取多个网址 - Stack Overflow Python urllib2.urlopen() 速度慢,需要更好的方法来读取多个网址

我在优化代码,展示如何使用线程并行获取多个网页。

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)

如你所见,应用程序特定的代码只有三行,如果你想简化的话,可以压缩成一行。我觉得没有人能证明这段代码复杂且难以维护。

不幸的是,这里大多数其他的线程代码都有一些缺陷。很多代码使用主动轮询来等待任务完成。使用 join() 方法来同步代码会更好。我认为这段代码在所有线程示例中都有所改进。

保持连接

WoLpH 提出的使用保持连接的建议,如果所有网址都指向同一服务器,可能会非常有用。

twisted

Aaron Gallagher 是 twisted 框架的粉丝,他对任何建议使用线程的人都持敌对态度。不幸的是,他的很多说法都是错误的。例如,他说:“-1,建议使用线程。这是 IO 密集型的;线程在这里没用。”这与事实相反,因为 Nick T 和我都证明了使用线程可以提高速度。实际上,IO 密集型的应用最能从使用 Python 的线程中获益(而 CPU 密集型的应用则没有收益)。Aaron 对线程的错误批评表明他对并行编程的理解相当混乱。

合适的工具用于合适的工作

我非常清楚使用线程、Python、异步 I/O 等进行并行编程时存在的问题。每种工具都有其优缺点。每种情况都有合适的工具。我并不反对 twisted(虽然我自己没有使用过)。但我不认为我们可以简单地说线程是坏的,而 twisted 是好的。

例如,如果提问者的要求是并行获取 10,000 个网站,异步 I/O 会更合适。线程在这种情况下可能不太合适(除非使用无栈 Python)。

Aaron 对线程的反对大多是泛化的。他没有意识到这是一个简单的并行化任务。每个任务都是独立的,并且不共享资源。因此,他的大部分攻击并不适用。

鉴于我的代码没有外部依赖,我认为这就是合适的工具用于合适的工作。

性能

我想大多数人会同意,这项任务的性能主要依赖于网络代码和外部服务器,而平台代码的性能影响微乎其微。然而,Aaron 的基准测试显示线程代码的速度提高了 50%。我认为有必要对这种明显的速度提升做出回应。

在 Nick 的代码中,有一个明显的缺陷导致了效率低下。但你怎么解释我的代码比他快了 233 毫秒呢?我认为即使是 twisted 的粉丝也会避免轻易下结论,把这归因于 twisted 的效率。毕竟,系统代码之外有大量变量,比如远程服务器的性能、网络、缓存,以及 urllib2 和 twisted web 客户端之间的不同实现等等。

为了确保 Python 的线程不会造成巨大的效率损失,我快速测试了启动 5 个线程和 500 个线程的情况。我可以很自信地说,启动 5 个线程的开销是微不足道的,无法解释 233 毫秒的速度差异。

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

对我并行获取的进一步测试显示,在 17 次运行中响应时间有很大的变化。(不幸的是,我没有 twisted 来验证 Aaron 的代码)。

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

我的测试并不支持 Aaron 的结论,即线程的速度始终比异步 I/O 慢,且差距可测。考虑到涉及的变量数量,我必须说这不是一个有效的测试,无法衡量异步 I/O 和线程之间的系统性能差异。

撰写回答