Как я могу ускорить получение страниц с urllib2 в Python? - PullRequest
26 голосов
/ 16 августа 2010

У меня есть скрипт, который выбирает несколько веб-страниц и анализирует информацию.

(пример можно увидеть на http://bluedevilbooks.com/search/?DEPT=MATH&CLASS=103&SEC=01)

Я запустил cProfile, и, как я предполагал, urlopen отнимает много времени. Есть ли способ получить страницы быстрее? Или способ получить несколько страниц одновременно? Я сделаю все самое простое, так как я новичок в разработке на Python и в Интернете.

Заранее спасибо! :)

ОБНОВЛЕНИЕ: у меня есть функция с именем fetchURLs(), которую я использую для создания массива нужных мне URL. что-то вроде urls = fetchURLS(). Все URL-адреса представляют собой файлы XML из API Amazon и eBay (что смущает меня, почему загрузка занимает так много времени, может быть, мой веб-хост работает медленно?)

Что мне нужно сделать, это загрузить каждый 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 ]

27 голосов
/ 16 августа 2010

РЕДАКТИРОВАТЬ : Я расширяю ответ, чтобы включить более отточенный пример. В этом посте я обнаружил много враждебности и дезинформации относительно потоков. асинхронный ввод / вывод. Поэтому я также добавил дополнительные аргументы, чтобы опровергнуть некоторые недействительные претензии Я надеюсь, что это поможет людям выбрать правильный инструмент для правильной работы.

Это ответ на вопрос 3 дня назад.

Python urllib2.open работает медленно, нужен лучший способ прочитать несколько URL-адресов - переполнение стека 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 строку, если вы агрессивны. Я не думаю, что кто-то может оправдать их утверждение, что это сложно и не поддается ремонту.

К сожалению, большинство других потоковых кодов, размещенных здесь, имеют некоторые недостатки. Многие из них проводят активный опрос, чтобы дождаться завершения кода. join() - лучший способ синхронизации кода. Я думаю, что этот код улучшил все примеры потоков.

поддержание связи

Предложение WoLpH об использовании keep-alive соединения может быть очень полезным, если все ваши URL-адреса указывают на один и тот же сервер.

витой

Аарон Галлахер - фанат фреймворка twisted и он враждебен ко всем, кто предлагает нить. К сожалению, многие его заявления являются дезинформацией. Например, он сказал «-1 для предложения потоков. Это связано с IO; потоки здесь бесполезны». Это противоречит доказательствам, поскольку и Ник Т, и я продемонстрировали увеличение скорости от использования потока. На самом деле приложение, связанное с вводом / выводом, имеет наибольшую выгоду от использования потока Python (v.s. нет выигрыша в приложении, связанном с процессором). Неправильная критика Аарона в отношении потоков показывает, что он довольно смущен параллельным программированием в целом.

Правильный инструмент для правильной работы

Мне хорошо известны проблемы, связанные с параллельным программированием с использованием потоков, Python, асинхронного ввода-вывода и так далее. У каждого инструмента есть свои плюсы и минусы. Для каждой ситуации есть соответствующий инструмент. Я не против крученого (хотя сам не развернул). Но я не верю, что мы можем сказать, что нить ПЛОХАЯ, а скрутка ХОРОША во всех ситуациях.

Например, если требование OP заключается в параллельном извлечении 10 000 веб-сайтов, предпочтительным будет асинхронный ввод-вывод. Потоки не будут подходящими (если не возможно с Python без стеков).

Противостояние Аарона нитям - это в основном обобщения. Он не понимает, что это тривиальная задача распараллеливания. Каждая задача независима и не разделяет ресурсы. Так что большинство его атак не применимо.

Поскольку мой код не имеет внешней зависимости, я назову его подходящим инструментом для правильной работы.

Производительность

Я думаю, что большинство людей согласится с тем, что производительность этой задачи во многом зависит от сетевого кода и внешнего сервера, где производительность кода платформы должна иметь незначительное влияние. Однако тест Аарона показывает увеличение скорости на 50% по сравнению с многопоточным кодом. Я думаю, что необходимо реагировать на это очевидное увеличение скорости.

В коде Ника есть очевидный недостаток, вызвавший неэффективность. Но как вы объясните увеличение скорости на 233 мс по сравнению с моим кодом? Я думаю, что даже фанаты скручивания будут воздерживаться от поспешных выводов, чтобы приписать это эффективности скрученного. В конце концов, существует огромный объем переменных вне системного кода, таких как производительность удаленного сервера, сеть, кэширование и реализация различий между urllib2 и витым веб-клиентом и т. Д.

Чтобы убедиться, что потоки 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 запусках.(К сожалению, я не проверял код Аарона).

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

Мое тестирование не подтверждает вывод Аарона о том, что многопоточность постоянно медленнее асинхронного ввода-вывода с измеримым запасом.Учитывая количество задействованных переменных, я должен сказать, что это недопустимый тест для измерения систематической разницы в производительности между асинхронным вводом-выводом и многопоточностью.

18 голосов
/ 16 августа 2010

Используйте витой ! Это делает такие вещи нелепо легкими по сравнению, скажем, с использованием потоков.

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

И, используя код Ника Т, настроил так, чтобы он также давал в среднем пять и показывал результат лучше:

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

Должен сказать, мне нравится, что последовательные выборки для меня лучше .

5 голосов
/ 16 августа 2010

Вот пример использования 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-адресов и будет зависеть от скорости вашего интернет-соединения и задержки на сервере. Находясь в Австралии, мой пинг> 300 мс

С 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
2 голосов
/ 20 декабря 2013

Большинство ответов было сосредоточено на одновременной загрузке нескольких страниц с разных серверов (многопоточность), но не на повторном использовании уже открытого HTTP-соединения.Если OP делает несколько запросов на один и тот же сервер / сайт.

В urlib2 создается отдельное соединение с каждым запросом, что влияет на производительность и, как следствие, меньшую скорость выборки страниц.urllib3 решает эту проблему с помощью пула соединений.Подробнее читайте здесь urllib3 [Также поточно-безопасный]

Существует также Запросы HTTP-библиотека, которая использует urllib3

Это в сочетании с многопоточностью должноувеличить скорость загрузки страниц

2 голосов
/ 16 августа 2010

Фактическое ожидание, вероятно, не в urllib2, а на сервере и / или в вашем сетевом подключении к серверу.

Есть 2 способа ускорить это.

  1. Поддерживать соединение живым (см. Этот вопрос о том, как это сделать: Python urllib2 с поддержкой активности )
  2. Используйте множественные соединения, вы можете использовать потоки или асинхронный подход, как предложил Аарон Галлахер. Для этого просто используйте любой пример потоков, и у вас все получится :) Вы также можете использовать библиотеку multiprocessing, чтобы упростить задачу.
1 голос
/ 05 февраля 2019

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 .

1 голос
/ 11 декабря 2015

Поскольку этот вопрос был опубликован, похоже, что доступна абстракция более высокого уровня, 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

1 голос
/ 27 января 2012

В настоящее время есть отличная библиотека Python, которая делает это для вас, и называется запросы .

Используйте стандартные API запросов, если вы хотите решение на основе потоков или асинхронного API (с использованием Gevent под капотом) если вы хотите решение, основанное на неблокирующем вводе-выводе.

0 голосов
/ 01 февраля 2017

Пожалуйста, найдите скрипт сетевого теста 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 легко идентифицируются.

0 голосов
/ 23 ноября 2014

Вот стандартное решение для библиотеки. Это не так быстро, но использует меньше памяти, чем многопоточные решения.

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-соединения, вероятно, поможет больше, чем параллельная работа.

...