Как ускорить выбор страниц с urllib2 в python?

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

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

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

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

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

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

Обратите внимание, что я не могу выполнить последнюю часть, пока не будет выбрано ВСЕ страницы, что моя проблема.

Кроме того, мой хост ограничивает меня до 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)

Ответы

Ответ 1

EDIT. Я расширяю ответ, чтобы включить более отполированный пример. В этой статье я нашел много враждебности и дезинформации в отношении пронизывания v.s. асинхронный ввод-вывод. Поэтому я также добавляю больше аргументов для опровержения некоторых недействительных требований. Надеюсь, это поможет людям выбрать правильный инструмент для правильной работы.

Это вопрос к 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-bound, нити здесь бесполезны". Это противоречит доказательству того, что и мы с Ником T и я продемонстрировали прирост скорости от используемой нити. На самом деле связанное с I/O приложение больше всего выигрывает от использования потока Python (нет увеличения в приложении с привязкой к процессору). Аарон вводил в заблуждение критику в отношении потоков, он довольно смущен в отношении параллельного программирования в целом.

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

Я хорошо знаю, что проблемы относятся к параллельному программированию с использованием потоков, python, async I/O и т.д. У каждого инструмента есть свои плюсы и минусы. Для каждой ситуации есть соответствующий инструмент. Я не против скручивания (хотя я сам не развернул его). Но я не верю, что мы можем сказать, что нить BAD и скрученная является ХОРОМ во всех ситуациях.

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

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

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

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

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

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

Просто чтобы убедиться, что потоки 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

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

Ответ 2

Используйте 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

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

Ответ 3

Вот пример использования 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 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

Ответ 4

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

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

  • Сохраняйте соединение в живых (см. этот вопрос о том, как это сделать: Python urllib2 с сохранением)
  • Используйте множественные соединения, вы можете использовать потоки или асинхронный подход, как предложил Аарон Галлахер. Для этого просто используйте любой пример нитей, и вы должны делать все правильно:) Вы также можете использовать multiprocessing lib, чтобы сделать вещи довольно легкими.

Ответ 5

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

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

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

В сочетании с поточной передачей следует увеличить скорость выборки страниц

Ответ 6

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

Используйте стандартный api запросов, если вы хотите решение на основе потоков или async api (используя gevent под капотом), если вы хотите решение на основе неблокирующего IO.

Ответ 7

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

Ответ 8

Получение веб-страниц, очевидно, займет некоторое время, так как вы не получаете доступ к чему-либо локальному. Если у вас есть несколько для доступа, вы можете использовать модуль 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

Схват данных из потока путем добавления к списку, вероятно, не рекомендуется (очередь будет лучше), но это иллюстрирует, что есть разница.

Ответ 9

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

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

Ответ 10

Найдите идентификатор сети Python script для идентификации медленного соединения:

"""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 легко идентифицируется.

Ответ 11

Ray предлагает элегантный способ сделать это (как в Python 2, так и в Python 3). Ray - это библиотека для написания параллельного и распределенного Python.

Просто определите функцию fetch с @ray.remote декоратора @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.