28

我有一个脚本可以获取多个网页并解析信息。

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

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

提前致谢!:)

更新:我有一个名为 的函数fetchURLs(),我用它来制作我需要的 URL 数组,urls = fetchURLS()比如我的虚拟主机很慢?)

我需要做的是加载每个 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)
4

11 回答 11

30

编辑:我正在扩展答案以包含一个更精美的示例。我在这篇文章中发现了很多关于线程与异步 I/O 的敌意和错误信息。因此,我还添加了更多论据来驳斥某些无效的主张。我希望这将帮助人们为正确的工作选择正确的工具。

这是3天前的一个问题。

Python urllib2.open 很慢,需要更好的方法来读取几个 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 行。我认为没有人可以证明他们声称这是复杂且不可维护的。

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

保持连接

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

扭曲的

Aaron Gallagher 是twisted框架的粉丝,他对任何建议线程的人都怀有敌意。不幸的是,他的许多说法都是错误的信息。例如,他说“-1 表示建议线程。这是 IO 绑定的;线程在这里没用。” 这与 Nick T 和我都证明了使用线程的速度增益的证据相反。事实上,I/O 绑定应用程序从使用 Python 的线程中获益最多(相对于 CPU 绑定应用程序没有任何收益)。Aaron 对线程的误导性批评表明,他对一般的并行编程相当困惑。

适合工作的正确工具

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

例如,如果 OP 的要求是并行获取 10,000 个网站,那么异步 I/O 将是首选。线程将不适用(除非可能使用无堆栈 Python)。

Aaron 对线程的反对主要是概括。他没有意识到这是一项微不足道的并行化任务。每个任务都是独立的,不共享资源。所以他的大部分攻击都不适用。

鉴于我的代码没有外部依赖性,我将其称为正确的工具来完成正确的工作。

表现

我想大多数人都会同意这项任务的性能很大程度上取决于网络代码和外部服务器,平台代码的性能应该可以忽略不计。然而,Aaron 的基准测试显示,与线程代码相比,速度提高了 50%。我认为有必要对这种明显的速度增益做出反应。

在尼克的代码中,有一个明显的缺陷导致效率低下。但是您如何解释我的代码的 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 次运行中响应时间存在巨大差异。(不幸的是,我没有扭曲来验证 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 和线程之间的系统性能差异的有效测试。

于 2010-08-16T06:05:20.963 回答
19

使用扭曲!与使用线程相比,它使这种事情变得非常容易。

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 的代码,也可以给出 5 的平均值并更好地显示输出:

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

我不得不说,我确实喜欢顺序提取对我来说表现更好

于 2010-08-16T03:20:01.090 回答
5

这是一个使用 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,很大程度上取决于您的互联网连接速度和服务器的延迟。在澳大利亚,我的 ping > 300ms

运行WORKERS=1时间为 86 秒 运行时间
WORKERS=423 秒 运行
时间WORKERS=10为 10 秒

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

这是使用队列的升级版本。至少有几个优点。
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
于 2010-08-16T07:57:46.243 回答
2

实际的等待可能不在服务器中,urllib2而是在服务器和/或您与服务器的网络连接中。

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

  1. 保持连接活动(有关如何做到这一点,请参阅此问题:Python urllib2 with keep alive
  2. 使用多重连接,您可以按照 Aaron Gallagher 的建议使用线程或异步方法。为此,只需使用任何线程示例就可以了:) 您还可以使用multiprocessinglib 使事情变得非常简单。
于 2010-08-16T02:08:49.047 回答
2

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

在 urllib2 中,每个请求都会创建一个单独的连接,这会影响性能,从而降低获取页面的速度。urllib3 通过使用连接池解决了这个问题。可以在这里阅读更多urllib3 [也是线程安全的]

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

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

于 2013-12-20T13:19:58.783 回答
2

由于发布了此问题,因此看起来有更高级别的抽象可用,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

于 2015-12-11T17:32:57.880 回答
1

现在有一个优秀的 Python 库可以为您执行此操作,称为requests

如果您想要基于线程的解决方案,请使用请求的标准 api;如果您想要基于非阻塞 IO 的解决方案,请使用异步 api(在后台使用 gevent)。

于 2012-01-27T13:31:00.277 回答
1

Ray提供了一种优雅的方式来做到这一点(在 Python 2 和 Python 3 中)。Ray 是一个用于编写并行和分布式 Python 的库。

只需使用装饰器定义fetch函数。@ray.remote然后,您可以通过调用在后台获取 URL fetch.remote(url)

import ray
import sys

ray.init()

@ray.remote
def fetch(url):
    if sys.version_info >= (3, 0):
        import urllib.request
        return urllib.request.urlopen(url).read()
    else:
        import urllib2
        return urllib2.urlopen(url).read()

urls = ['https://en.wikipedia.org/wiki/Donald_Trump',
        'https://en.wikipedia.org/wiki/Barack_Obama',
        'https://en.wikipedia.org/wiki/George_W._Bush',
        'https://en.wikipedia.org/wiki/Bill_Clinton',
        'https://en.wikipedia.org/wiki/George_H._W._Bush']

# Fetch the webpages in parallel.
results = ray.get([fetch.remote(url) for url in urls])

如果你还想并行处理网页,可以直接将处理代码放入fetch,也可以定义一个新的远程函数并组合在一起。

@ray.remote
def process(html):
    tokens = html.split()
    return set(tokens)

# Fetch and process the pages in parallel.
results = []
for url in urls:
    results.append(process.remote(fetch.remote(url)))
results = ray.get(results)

如果您想要获取的 URL 列表很长,您可能希望发出一些任务,然后按照它们完成的顺序处理它们。您可以使用ray.wait.

urls = 100 * urls  # Pretend we have a long list of URLs.
results = []

in_progress_ids = []

# Start pulling 10 URLs in parallel.
for _ in range(10):
    url = urls.pop()
    in_progress_ids.append(fetch.remote(url))

# Whenever one finishes, start fetching a new one.
while len(in_progress_ids) > 0:
    # Get a result that has finished.
    [ready_id], in_progress_ids = ray.wait(in_progress_ids)
    results.append(ray.get(ready_id))
    # Start a new task.
    if len(urls) > 0:
        in_progress_ids.append(fetch.remote(urls.pop()))

查看Ray 文档

于 2019-02-05T02:33:17.647 回答
0

获取网页显然需要一段时间,因为您没有访问任何本地内容。如果您有几个要访问,您可以使用该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

通过附加到列表来从线程中获取数据可能是不明智的(队列会更好),但它说明存在差异。

于 2010-08-16T02:08:53.253 回答
0

这是一个标准库解决方案。它不是那么快,但它比线程解决方案使用更少的内存。

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 连接可能比并行处理更有用。

于 2014-11-23T16:02:36.880 回答
0

请查找 Python 网络基准脚本进行单连接慢速识别:

"""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 缓慢。

于 2017-02-01T18:28:13.693 回答