いくつかのWebページをフェッチして情報を解析するスクリプトがあります。
(例は http://bluedevilbooks.com/search/?DEPT=MATH&CLASS=103&SEC=01 で見ることができます)
その上でcProfileを実行しましたが、予想どおり、urlopenには多くの時間がかかります。ページをより速くフェッチする方法はありますか?または、一度に複数のページをフェッチする方法はありますか? pythonとWeb開発に慣れていないので、最も簡単なことは何でもします。
前もって感謝します! :)
更新:fetchURLs()
という関数があります。これを使用して必要なURLの配列を作成し、urls = fetchURLS()
のようにします。URLはすべてAmazonおよびeBay APIからの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)
[〜#〜]編集[〜#〜]:より洗練された例を含めるために答えを拡張しています。私はこの投稿でスレッドv.sに関して多くの敵意と誤った情報を見つけました。非同期I/O。したがって、私はまた、特定の無効な主張に反論するための議論を追加します。これが人々が適切な仕事に適切なツールを選択するのに役立つことを願っています。
これは3日前の質問の重複です。
Python urllib2.openは遅いので、いくつかのURLを読み取るためのより良い方法が必要です-Stack Overflow Python urllib2.urlopen()は遅いので、いくつかのURLを読み取るためのより良い方法が必要です
スレッドを使用して複数のWebページを並行してフェッチする方法を示すためにコードを磨いています。
_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()
は、コードを同期するためのより良い方法です。このコードは、これまでのすべてのスレッド例を改善したと思います。
キープアライブ接続
キープアライブ接続の使用に関するWoLpHの提案は、すべてのURLが同じサーバーを指している場合に非常に役立つ可能性があります。
ツイスト
AaronGallagherはtwisted
フレームワークのファンであり、スレッドを提案する人には敵対的です。残念ながら、彼の主張の多くは誤った情報です。たとえば、彼は「スレッドを提案するための-1。これはIOバウンドです。スレッドはここでは役に立たない」と述べました。これは、ニックTと私がスレッドを使用することで速度が向上することを示したという証拠とは反対です。実際、I/Oバウンドアプリケーションは、Pythonのスレッドを使用することで最も多くの利益を得ることができます(CPUバウンドアプリケーションでは利益がありません)。アーロンのスレッドに対する誤った批判は、彼が一般的な並列プログラミングについてかなり混乱していることを示しています。
適切な仕事のための適切なツール
私は、スレッド、Python、非同期I/Oなどを使用した並列プログラミングに関連する問題をよく知っています。各ツールには長所と短所があります。それぞれの状況に適したツールがあります。私はねじれに反対していません(私は自分で展開していませんが)。しかし、すべての状況で糸が悪く、ねじれが良いと言っても過言ではないと思います。
たとえば、OPの要件が10,000のWebサイトを並行してフェッチすることである場合、非同期I/Oが優先されます。スレッド化は適切ではありません(おそらくスタックレスPythonを使用しない限り)。
アーロンのスレッドに対する反対は、ほとんどが一般化です。彼は、これが簡単な並列化タスクであることを認識していません。各タスクは独立しており、リソースを共有しません。したがって、彼の攻撃のほとんどは適用されません。
私のコードには外部依存関係がないので、適切な仕事に適したツールと呼びます。
パフォーマンス
ほとんどの人は、このタスクのパフォーマンスがネットワークコードと外部サーバーに大きく依存していることに同意すると思います。プラットフォームコードのパフォーマンスによる影響はごくわずかです。ただし、Aaronのベンチマークは、スレッド化されたコードよりも50%の速度向上を示しています。この見かけの速度向上に対応する必要があると思います。
Nickのコードには、非効率性の原因となった明らかな欠陥があります。しかし、私のコードよりも233msの速度向上をどのように説明しますか?ツイストファンでさえ、これをツイストの効率に帰するために結論に飛び込むことを控えると思います。結局のところ、リモートサーバーのパフォーマンス、ネットワーク、キャッシング、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
_
並列フェッチをさらにテストすると、17回の実行で応答時間に大きなばらつきがあることがわかります。 (残念ながら、私はアーロンのコードを検証するためにひねりを加えていません)。
_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
_
私のテストは、スレッド化が非同期I/Oよりも測定可能なマージンで一貫して遅いというAaronの結論をサポートしていません。関係する変数の数を考えると、これは非同期I/Oとスレッド化の間の体系的なパフォーマンスの違いを測定するための有効なテストではないと言わざるを得ません。
使用 ツイスト !これにより、たとえばスレッドを使用する場合に比べて、この種のことが非常に簡単になります。
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
そして、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
私は、シーケンシャルフェッチがより良く実行されるのが好きだと言わざるを得ません。
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=4
_では、実行に23秒かかりました
_WORKERS=10
_を使用すると、実行に10秒かかりました
したがって、10個のスレッドをダウンロードすると、1個のスレッドの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
_
回答のほとんどは、異なるサーバーから同時に複数のページをフェッチすること(スレッド化)に焦点を当てていましたが、すでに開いているHTTP接続を再利用することには焦点を当てていませんでした。 OPが同じサーバー/サイトに複数のリクエストを行っている場合。
Urlib2では、リクエストごとに個別の接続が作成され、パフォーマンスに影響を与え、その結果、ページのフェッチ速度が低下します。 urllib3は、接続プールを使用してこの問題を解決します。詳細はこちら rllib [スレッドセーフも]
Requests urllib3を使用するHTTPライブラリもあります
これをスレッド化と組み合わせると、ページのフェッチ速度が向上します。
実際の待機は、おそらくurllib2
ではなく、サーバーおよび/またはサーバーへのネットワーク接続にあります。
これを高速化する方法は2つあります。
multiprocessing
libを使用して、物事を非常に簡単にすることもできます。この質問が投稿されたので、より高いレベルの抽象化が利用できるようです、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
Ray は、これを行うためのエレガントな方法を提供します(Python 2とPython 3)の両方で)。Rayは並列書き込み用のライブラリです。分散Python。
fetch
関数を_@ray.remote
_デコレータで定義するだけです。次に、fetch.remote(url)
を呼び出すことにより、バックグラウンドで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])
_
Webページも並行して処理する場合は、処理コードを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ドキュメント を表示します。
今日では、 requests と呼ばれる優れたPython libがこれを実行します。
スレッドに基づくソリューションが必要な場合はリクエストの標準APIを使用し、非ブロッキングIOに基づくソリューションが必要な場合は非同期API(内部でgeventを使用)を使用します。
これが標準ライブラリソリューションです。それほど高速ではありませんが、スレッドソリューションよりも少ないメモリを使用します。
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接続を再利用することは、並行して行うよりも役立つ可能性があります。
ローカルにアクセスしていないため、Webページのフェッチには明らかに時間がかかります。アクセスするものが複数ある場合は、 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
リストに追加してスレッドからデータを取得することはおそらくお勧めできませんが(キューの方が良いでしょう)、違いがあることを示しています。
単一接続の速度低下を特定するための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
Python2.7.13の結果は非常に似ています。
この場合、DNSとurlopenの速度低下は簡単に識別できます。