Каков самый быстрый способ отправки 100 000 HTTP-запросов в Python?

Я открываю файл с 100 000 URL. Мне нужно отправить HTTP-запрос на каждый URL и распечатать код состояния. Я использую Python 2.6, и до сих пор смотрел на многие запутанные способы, которыми Python реализует многопоточность/параллелизм. Я даже посмотрел на библиотеку Python Concurrence, но не могу понять, как правильно написать эту программу. Кто-нибудь сталкивался с подобной проблемой? Я предполагаю, что в целом мне нужно знать, как выполнить тысячи задач в Python как можно быстрее - я полагаю, это означает "одновременно".

Ответ 1

Беспроблемное решение:

from urlparse import urlparse
from threading import Thread
import httplib, sys
from Queue import Queue

concurrent = 200

def doWork():
    while True:
        url = q.get()
        status, url = getStatus(url)
        doSomethingWithResult(status, url)
        q.task_done()

def getStatus(ourl):
    try:
        url = urlparse(ourl)
        conn = httplib.HTTPConnection(url.netloc)   
        conn.request("HEAD", url.path)
        res = conn.getresponse()
        return res.status, ourl
    except:
        return "error", ourl

def doSomethingWithResult(status, url):
    print status, url

q = Queue(concurrent * 2)
for i in range(concurrent):
    t = Thread(target=doWork)
    t.daemon = True
    t.start()
try:
    for url in open('urllist.txt'):
        q.put(url.strip())
    q.join()
except KeyboardInterrupt:
    sys.exit(1)

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

Ответ 2

Решение с использованием tornado асинхронной сетевой библиотеки

from tornado import ioloop, httpclient

i = 0

def handle_request(response):
    print(response.code)
    global i
    i -= 1
    if i == 0:
        ioloop.IOLoop.instance().stop()

http_client = httpclient.AsyncHTTPClient()
for url in open('urls.txt'):
    i += 1
    http_client.fetch(url.strip(), handle_request, method='HEAD')
ioloop.IOLoop.instance().start()

Ответ 3

Темы здесь абсолютно не отвечают. Они будут обеспечивать узкие места процесса и ядра, а также ограничения пропускной способности, которые неприемлемы, если общая цель - "самый быстрый способ".

Немного twisted и его асинхронный клиент HTTP дадут вам гораздо лучшие результаты.

Ответ 4

С 2010 года ситуация изменилась с самого начала, и я не пробовал все остальные ответы, но я попробовал несколько, и я нашел, что это работает для меня лучше, используя python3.6.

Я смог получить около 150 уникальных доменов в секунду, работающих на AWS.

import pandas as pd
import concurrent.futures
import requests
import time

out = []
CONNECTIONS = 100
TIMEOUT = 5

tlds = open('../data/sample_1k.txt').read().splitlines()
urls = ['http://{}'.format(x) for x in tlds[1:]]

def load_url(url, timeout):
    ans = requests.head(url, timeout=timeout)
    return ans.status_code

with concurrent.futures.ThreadPoolExecutor(max_workers=CONNECTIONS) as executor:
    future_to_url = (executor.submit(load_url, url, TIMEOUT) for url in urls)
    time1 = time.time()
    for future in concurrent.futures.as_completed(future_to_url):
        try:
            data = future.result()
        except Exception as exc:
            data = str(type(exc))
        finally:
            out.append(data)

            print(str(len(out)),end="\r")

    time2 = time.time()

print(f'Took {time2-time1:.2f} s')
print(pd.Series(out).value_counts())

Ответ 5

Используйте grequests, это комбинация запросов + модуль Gevent.

GRequests позволяет использовать Запросы с Gevent для упрощения асинхронных запросов HTTP.

Использование прост:

import grequests

urls = [
   'http://www.heroku.com',
   'http://tablib.org',
   'http://httpbin.org',
   'http://python-requests.org',
   'http://kennethreitz.com'
]

Создайте набор неотправленных запросов:

>>> rs = (grequests.get(u) for u in urls)

Отправляйте их одновременно:

>>> grequests.map(rs)
[<Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>]

Ответ 6

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

В идеальном мире это просто означало бы одновременное начало 100 000 потоков, которые выводят их результаты в словарь или список для последующей обработки, но на практике вы ограничены в том, сколько параллельных HTTP-запросов вы можете опубликовать таким образом. Локально у вас есть ограничения в том, сколько сокетов вы можете открывать одновременно, сколько потоков выполнения вашего интерпретатора Python позволит. Удаленно, вы можете ограничить количество одновременных подключений, если все запросы связаны с одним сервером или многими. Эти ограничения, вероятно, потребуют, чтобы вы записали script таким образом, чтобы только опросить небольшую часть URL-адресов в любой момент времени (100, как упоминалось в другом плакате, вероятно, является приличным размером пула потоков, хотя вы можете найти что вы можете успешно развернуть еще много).

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

  • Запустите поток, который запускает новые потоки запросов до тех пор, пока количество текущих выполняемых потоков (вы можете отследить их через threading.active_count() или нажав объекты потока в структуру данных) >= максимальное количество одновременных запросов ( скажем, 100), затем спит на короткий тайм-аут. Этот поток должен завершиться, когда больше нет URL-адресов для обработки. Таким образом, поток будет продолжать просыпаться, запускать новые потоки и спать, пока вы не закончите.
  • Пусть потоки запросов сохраняют свои результаты в некоторой структуре данных для последующего поиска и вывода. Если структура, в которой вы сохраняете результаты, является list или dict в CPython, вы можете безопасно добавлять или вставлять уникальные элементы из ваших потоков без блокировок, но если вы пишете файл или требуете более сложного межпоточного взаимодействия данных, вы должны использовать блокировку взаимного исключения, чтобы защитить это состояние от повреждения.

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

Наконец, если вы хотите увидеть довольно простое приложение параллельного сетевого приложения, написанное на Python, посмотрите ssh.py, Это небольшая библиотека, которая использует потоки Python для параллелизации многих SSH-соединений. Дизайн достаточно близок к вашим требованиям, чтобы вы могли найти его хорошим ресурсом.

Ответ 7

Если вы хотите получить максимальную производительность, вам может потребоваться использовать асинхронный ввод-вывод, а не потоки. Накладные расходы, связанные с тысячами потоков ОС, являются нетривиальными, а переключение контекста в интерпретаторе Python добавляет еще больше к нему. Threading, безусловно, выполнит эту работу, но я подозреваю, что асинхронный маршрут обеспечит лучшую общую производительность.

В частности, я предлагаю асинхронный веб-клиент в Twisted-библиотеке (http://www.twistedmatrix.com). У этого есть, по общему признанию, крутая кривая обучения, но он довольно прост в использовании, когда вы получаете хорошую ручку в стиле Twisted асинхронного программирования.

A HowTo по авизованному API асинхронного веб-клиента доступен по адресу:

http://twistedmatrix.com/documents/current/web/howto/client.html

Ответ 8

Решение:

from twisted.internet import reactor, threads
from urlparse import urlparse
import httplib
import itertools


concurrent = 200
finished=itertools.count(1)
reactor.suggestThreadPoolSize(concurrent)

def getStatus(ourl):
    url = urlparse(ourl)
    conn = httplib.HTTPConnection(url.netloc)   
    conn.request("HEAD", url.path)
    res = conn.getresponse()
    return res.status

def processResponse(response,url):
    print response, url
    processedOne()

def processError(error,url):
    print "error", url#, error
    processedOne()

def processedOne():
    if finished.next()==added:
        reactor.stop()

def addTask(url):
    req = threads.deferToThread(getStatus, url)
    req.addCallback(processResponse, url)
    req.addErrback(processError, url)   

added=0
for url in open('urllist.txt'):
    added+=1
    addTask(url.strip())

try:
    reactor.run()
except KeyboardInterrupt:
    reactor.stop()

Testtime:

[[email protected]:~] wc -l urllist.txt
10000 urllist.txt
[[email protected]:~] time python f.py > /dev/null 

real    1m10.682s
user    0m16.020s
sys 0m10.330s
[[email protected]:~] head -n 6 urllist.txt
http://www.google.com
http://www.bix.hu
http://www.godaddy.com
http://www.google.com
http://www.bix.hu
http://www.godaddy.com
[[email protected]:~] python f.py | head -n 6
200 http://www.bix.hu
200 http://www.bix.hu
200 http://www.bix.hu
200 http://www.bix.hu
200 http://www.bix.hu
200 http://www.bix.hu

Pingtime:

bix.hu is ~10 ms away from me
godaddy.com: ~170 ms
google.com: ~30 ms

Ответ 9

Я знаю, что это старый вопрос, но в Python 3.7 вы можете сделать это, используя asyncio и aiohttp.

import asyncio
import aiohttp
from aiohttp import ClientSession, ClientConnectorError

async def fetch_html(url: str, session: ClientSession, **kwargs) -> tuple:
    try:
        resp = await session.request(method="GET", url=url, **kwargs)
    except ClientConnectorError:
        return (url, 404)
    return (url, resp.status)

async def make_requests(urls: set, **kwargs) -> None:
    async with ClientSession() as session:
        tasks = []
        for url in urls:
            tasks.append(
                fetch_html(url=url, session=session, **kwargs)
            )
        results = await asyncio.gather(*tasks)

    for result in results:
        print(f'{result[1]} - {str(result[0])}')

if __name__ == "__main__":
    import pathlib
    import sys

    assert sys.version_info >= (3, 7), "Script requires Python 3.7+."
    here = pathlib.Path(__file__).parent

    with open(here.joinpath("urls.txt")) as infile:
        urls = set(map(str.strip, infile))

    asyncio.run(make_requests(urls=urls))

Вы можете узнать больше об этом и посмотреть пример здесь.

Ответ 10

Использование пула потоков является хорошим вариантом и сделает это довольно легко. К сожалению, у python нет стандартной библиотеки, которая упрощает пулы потоков. Но вот приличная библиотека, которая должна вас начать: http://www.chrisarndt.de/projects/threadpool/

Пример кода с сайта:

pool = ThreadPool(poolsize)
requests = makeRequests(some_callable, list_of_args, callback)
[pool.putRequest(req) for req in requests]
pool.wait()

Надеюсь, что это поможет.

Ответ 11

Создать объект epoll,
открыть много клиентских TCP-сокетов,
настроить свои буферы отправки так, чтобы они были немного больше заголовка запроса,
отправьте заголовок запроса - он должен быть немедленным, просто помещенным в буфер, зарегистрировать сокет в объекте epoll,
сделать .poll на epoll obect,
читать первые 3 байта из каждого сокета из .poll,
запишите их в sys.stdout затем \n (не сбрасывать), закройте клиентский сокет.

Ограничить количество открытых сокетов одновременно - обрабатывать ошибки при создании сокетов. Создайте новый сокет, только если другой закрыт.
Настройте пределы ОС.
Попробуйте разделить несколько (не много) процессов: это может помочь немного эффективнее использовать процессор.

Ответ 12

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

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

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

Ответ 13

Рассмотрите возможность использования Windmill, хотя Windmill, вероятно, не может сделать это много потоков.

Вы можете сделать это с помощью ручного Python script на 5 машинах, каждый из которых подключается исходящим, используя порты 40000-60000, открывая 100 000 портов.

Кроме того, это может помочь сделать образец теста с красивым приложением QA, таким как OpenSTA, чтобы получить представление от того, сколько может обрабатывать каждый сервер.

Кроме того, попробуйте просто использовать простой Perl с классом LWP:: ConnCache. Вероятно, вы получите больше производительности (больше подключений).

Ответ 14

Этот скрученный веб-клиент async работает довольно быстро.

#!/usr/bin/python2.7

from twisted.internet import reactor
from twisted.internet.defer import Deferred, DeferredList, DeferredLock
from twisted.internet.defer import inlineCallbacks
from twisted.web.client import Agent, HTTPConnectionPool
from twisted.web.http_headers import Headers
from pprint import pprint
from collections import defaultdict
from urlparse import urlparse
from random import randrange
import fileinput

pool = HTTPConnectionPool(reactor)
pool.maxPersistentPerHost = 16
agent = Agent(reactor, pool)
locks = defaultdict(DeferredLock)
codes = {}

def getLock(url, simultaneous = 1):
    return locks[urlparse(url).netloc, randrange(simultaneous)]

@inlineCallbacks
def getMapping(url):
    # Limit ourselves to 4 simultaneous connections per host
    # Tweak this number, but it should be no larger than pool.maxPersistentPerHost 
    lock = getLock(url,4)
    yield lock.acquire()
    try:
        resp = yield agent.request('HEAD', url)
        codes[url] = resp.code
    except Exception as e:
        codes[url] = str(e)
    finally:
        lock.release()


dl = DeferredList(getMapping(url.strip()) for url in fileinput.input())
dl.addCallback(lambda _: reactor.stop())

reactor.run()
pprint(codes)

Ответ 15

Самый простой способ - использовать встроенную библиотеку потоков Python. Они не являются "реальными"/ядерными потоками. У них есть проблемы (например, сериализация), но они достаточно хороши. Вам нужен пул очереди и потока. Один из вариантов здесь, но тривиально писать свои собственные. Вы не можете распараллелить все 100 000 звонков, но вы можете одновременно запустить 100 (или около того) из них.