Pythonでurllib2を使用してページを速めるにはどうすればよいですか?
質問
いくつかのWebページを取得し、情報を解析するスクリプトがあります。
(例を見ることができます http://bluedevilbooks.com/search/?dept=math&class=103&sec=01 )
私はそれでcrofileを走らせました、そして、私が仮定したように、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)
解決
編集: :答えを拡張して、より洗練された例を含めています。この投稿では、スレッディングとasync i/oに関する敵意と誤った情報がたくさんありました。したがって、特定の無効な主張に反論するための議論を追加します。これが人々が適切な仕事に適したツールを選択するのに役立つことを願っています。
これは3日前の質問へのdupです。
python urllib2.openは遅く、いくつかのURLを読むためのより良い方法が必要です - スタックオーバーフロー 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()
コードを同期するためのより良い方法です。このコードは、これまでのすべてのスレッドの例を改善したと思います。
アリブな接続を維持します
Keep-Alive接続の使用に関するWolphの提案は、URLがすべて同じサーバーを指している場合、非常に便利です。
ねじれた
アーロン・ギャラガーはファンです twisted
フレームワークと彼は、スレッドを提案する人々を敵対的にしています。残念ながら、彼の主張の多くは誤った情報です。たとえば、彼は「スレッドを提案するために-1。これはIOバウンドです。ここではスレッドは役に立たない」と言いました。ニックTと私の両方が使用したスレッドからのスピードゲインを実証したという証拠に反しています。実際、I/O Boundアプリケーションは、Pythonのスレッドを使用することで得られるものが最も多くあります(CPUバウンドアプリケーションではゲインなし)。アーロンのスレッドに対する見当違いの批判は、彼が一般的に並行するプログラミングについてかなり混乱していることを示しています。
適切な仕事に適したツール
スレッド、Python、Async I/Oなどを使用した並列プログラミングに関連する問題をよく知っています。各ツールには長所と短所があります。各状況には、適切なツールがあります。私はツイストに反対していません(私は自分で展開していませんが)。しかし、私たちは、すべての状況でスレッドが悪いとねじれていると言うことができるとは思わないと思います。
たとえば、OPの要件が10,000のWebサイトを並行して取得することである場合、非同期I/Oが好ましいでしょう。スレッドは適切ではありません(Stackless Pythonを使用しない限り)。
アーロンのスレッドに対する反対は、主に一般化です。彼は、これが些細な並列化タスクであることを認識していません。各タスクは独立しており、リソースを共有していません。したがって、彼の攻撃のほとんどは適用されません。
私のコードに外部依存関係がないことを考えると、私はそれを正しい仕事に向けて正しいツールと呼びます。
パフォーマンス
ほとんどの人は、このタスクのパフォーマンスがネットワークコードと外部サーバーに大きく依存していることに同意すると思います。ただし、アーロンのベンチマークは、スレッドコードよりも50%の速度ゲインを示しています。この明らかな速度ゲインに対応する必要があると思います。
ニックのコードには、非効率性を引き起こす明らかな欠陥があります。しかし、私のコードよりも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よりも一貫して遅くなるというアーロンの結論をサポートしていません。関係する変数の数を考えると、これは非同期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は300mを超えています
と WORKERS=1
実行に86秒かかりました
と WORKERS=4
実行に23秒かかりました
と 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
実際の待機はおそらくありません urllib2
ただし、サーバーおよび/またはサーバーへのネットワーク接続で。
これをスピードアップする方法は2つあります。
- 接続を生かし続けます(それを行う方法についてのこの質問を参照してください: Python urllib2が生存しています)
- Multiplle Connectionsを使用して、Aaron Gallagherが提案したように、スレッドまたは非同期アプローチを使用できます。そのためには、任意のスレッドの例を使用するだけで、正常に行う必要があります:)
multiprocessing
物事をとても簡単にするためのlib。
回答のほとんどは、異なるサーバーから複数のページを同時に(スレッド)に取得することに焦点を当てていましたが、すでに開いているHTTP接続を再利用することはありませんでした。 OPが同じサーバー/サイトに複数のリクエストを行っている場合。
urlib2では、パフォーマンスに影響を与え、その結果、ページを取得するレートが遅い各要求で別の接続が作成されます。 urllib3は、接続プールを使用してこの問題を解決します。こちらをご覧ください urllib3 スレッドセーフも
もあります リクエスト urllib3を使用するHTTPライブラリ
これと組み合わせると、ページを取得する速度が向上するはずです
今日、あなたのためにこれを行う素晴らしいPython libがあります リクエスト.
スレッドまたはAsync API(フードの下でGeventを使用)に基づいてソリューションが必要な場合は、リクエストの標準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
レイ これを行うためのエレガントな方法を提供します(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])
また、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()))
を表示します レイのドキュメント.
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
リストに追加することでスレッドからデータを取得することはおそらく賢明ではないでしょう(キューの方が良いでしょう)が、違いがあることを示しています。
これが標準的なライブラリソリューションです。それほど速くはありませんが、スレッドソリューションよりも少ないメモリを使用します。
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接続を再利用することは、おそらく並行して物事を行う以上のものに役立つでしょう。
単一接続の遅延識別については、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の遅さは簡単に識別されます。