Qual é a maneira mais rápida de enviar 100.000 solicitações HTTP em Python?

286

Estou abrindo um arquivo com 100.000 URLs. Preciso enviar uma solicitação HTTP para cada URL e imprimir o código de status. Estou usando o Python 2.6 e, até agora, olhei para as muitas maneiras confusas em que o Python implementa threading / simultaneidade. Eu até olhei para a biblioteca de concorrência python , mas não consigo descobrir como escrever este programa corretamente. Alguém já se deparou com um problema semelhante? Acho que geralmente preciso saber como executar milhares de tarefas no Python o mais rápido possível - suponho que isso signifique 'simultaneamente'.

IgorGanapolsky
fonte
47
Certifique-se de fazer apenas a solicitação HEAD (para não baixar o documento inteiro). Veja: stackoverflow.com/questions/107405/…
Tarnay Kálmán
5
Ponto excelente, Kalmi. Se tudo o que Igor deseja é o status da solicitação, essas solicitações de 100 mil serão muito, muito, muito mais rápidas. Muito mais rápido.
precisa
1
Você não precisa de threads para isso; a maneira mais eficiente provavelmente usará uma biblioteca assíncrona como Twisted.
jemfinch
3
aqui estão GEvent, torcido, e asyncio exemplos de código baseados (testado em 1000000 pedidos)
JFS
4
@ TarnayKálmán seu possível para requests.gete requests.head(ou seja, uma solicitação de página vs um pedido cabeça) para retornar diferentes códigos de status, de modo que este não é o melhor conselho
AlexG

Respostas:

201

Solução Twistedless:

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)

Este é um pouco mais rápido que a solução distorcida e usa menos CPU.

Tarnay Kálmán
fonte
10
@ Kalmi, por que você define a fila como concurrent*2?
Marcel Wilson
8
Não esqueça de fechar a conexão conn.close() . Abrir muitas conexões http pode interromper seu script em algum momento e consumir memória.
Aamir Adnan
4
@hyh, o Queuemódulo foi renomeado para queueno Python 3. Esse é o código do Python 2.
Tarnay Kálmán
3
Quanto mais rápido você pode ir se quiser conversar com o mesmo servidor todas as vezes, persistindo na conexão? Isso pode ser feito através de threads ou com uma conexão persistente por thread?
mdurant
2
@mptevsion, se você estiver usando o CPython, poderá (por exemplo) substituir apenas "status de impressão, url" por "my_global_list.append ((status, url))". As listas (a maioria das operações ativadas) são implicitamente seguras contra encadeamento no CPython (e em algumas outras implementações de python) devido ao GIL, portanto é seguro fazê-lo.
Tarnay Kálmán
54

Uma solução usando a biblioteca de rede assíncrona 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()
mher
fonte
7
Este código está usando E / S de rede sem bloqueio e não possui nenhuma restrição. Pode ser dimensionado para dezenas de milhares de conexões abertas. Ele será executado em um único thread, mas será muito mais rápido que qualquer solução de encadeamento. E / S sem bloqueio do Google Checkout en.wikipedia.org/wiki/Asynchronous_I/O
check out mher
1
Você pode explicar o que está acontecendo aqui com a variável i global? Algum tipo de verificação de erro?
LittleBobbyTables
4
É um contador para determinar quando sair do `` ioloop` - portanto, quando terminar.
Michael Dorner
1
@AndrewScottEvans presume que você está usando Python 2.7 e proxies
Dejell
5
@ Buy Avraham Boa sorte em obter ajuda em seu plano de ddos.
Walter Walter
50

As coisas mudaram bastante desde 2010, quando isso foi publicado e eu não tentei todas as outras respostas, mas tentei algumas e achei que funcionava melhor para mim usando python3.6.

Consegui buscar cerca de 150 domínios únicos por segundo em execução na 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())
Glen Thompson
fonte
1
Só estou perguntando porque não sei, mas esse material futuro pode ser substituído por async / waitit?
TankorSmash
1
Poderia, mas eu encontrei o acima para funcionar melhor. você pode usar o aiohttp, mas não faz parte da lib padrão e está mudando bastante. Funciona, mas não achei que funcionasse também. Eu obtenho taxas de erro mais altas quando o uso e, durante toda a minha vida, não consigo que funcione tão bem quanto futuros concorrentes, embora, em teoria, pareça que funcione melhor, consulte: stackoverflow.com/questions/45800857/… se você conseguir que ele funcione bem, poste sua resposta para que eu possa testá-lo.
Glen Thompson
1
Este é um detalhe, mas acho que é muito mais fácil colocar time1 = time.time()no topo do loop for e time2 = time.time()logo após o loop for.
Matt M.
Testei seu snippet, de alguma forma ele é executado duas vezes. Estou fazendo algo errado? Ou é para rodar duas vezes? Se for o último caso, você também pode me ajudar a entender como ele dispara duas vezes?
Ronnie
1
Não deve ser executado duas vezes. Não sei por que você está vendo isso.
Glen Thompson
40

Threads não são absolutamente a resposta aqui. Eles fornecerão gargalos no processo e no kernel, além de limites de taxa de transferência que não são aceitáveis ​​se o objetivo geral for "o caminho mais rápido".

Um pouco twistede seu HTTPcliente assíncrono forneceriam resultados muito melhores.

ironfroggy
fonte
ironfroggy: Estou me inclinando para seus sentimentos. Tentei implementar minha solução com threads e filas (para mutexes automáticos), mas você pode imaginar quanto tempo leva para preencher uma fila com 100.000 coisas? Ainda estou brincando com diferentes opções e sugestões de todos neste tópico, e talvez o Twisted seja uma boa solução.
IgorGanapolsky
2
Você pode evitar preencher uma fila com 100 mil itens. Apenas processe os itens um de cada vez a partir da sua entrada e, em seguida, inicie um encadeamento para processar a solicitação correspondente a cada item. (Como descrevo abaixo, use um encadeamento do iniciador para iniciar os encadeamentos de solicitação HTTP quando a contagem de encadeamentos estiver abaixo de algum limite. Faça com que os encadeamentos escrevam os resultados em um URL de mapeamento de ditados para resposta ou anexem tuplas a uma lista.)
Erik Garrison
ironfroggy: Além disso, estou curioso para saber quais gargalos você encontrou usando os threads do Python? E como os threads do Python interagem com o kernel do SO?
Erik Garrison
Certifique-se de instalar o reator epoll; caso contrário, você usará select / poll, e será muito lento. Além disso, se você realmente tentar abrir 100.000 conexões simultaneamente (supondo que seu programa seja escrito dessa maneira e os URLs estejam em servidores diferentes), será necessário ajustar o sistema operacional para não esgotar de descritores de arquivos, portas efêmeras, etc. (provavelmente é mais fácil garantir que você não tenha mais do que, digamos, 10.000 conexões pendentes ao mesmo tempo).
Mark Nottingham
erikg: você recomendou uma ótima idéia. No entanto, o melhor resultado que consegui com 200 threads foi de aprox. 6 minutos. Eu tenho certeza que existem maneiras de fazer isso em menos tempo ... Mark N: se Twisted é o caminho que eu decido seguir, então o reator epoll é certamente útil. No entanto, se meu script for executado em várias máquinas, isso não exigiria a instalação do Twisted em CADA máquina? Não sei se consigo convencer meu chefe a seguir esse caminho ...
IgorGanapolsky 27/04
20

Eu sei que essa é uma pergunta antiga, mas no Python 3.7 você pode fazer isso usando asyncioe 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))

Você pode ler mais sobre isso e ver um exemplo aqui .

Marius Stănescu
fonte
É semelhante ao C # async / waitit e Kotlin Coroutines?
IgorGanapolsky 28/08/19
@IgorGanapolsky, sim, é muito semelhante ao C # assíncrono / espera. Não estou familiarizado com a Kotlin Coroutines.
Marius Stănescu
@ Sandyp, não tenho certeza se funciona, mas se você quiser tentar, terá que usar o UnixConnector para aiohttp. Leia mais aqui: docs.aiohttp.org/en/stable/client_reference.html#connectors .
Marius Stănescu
Obrigado @ MariusStănescu. Isso é exatamente o que eu usei.
sandyp
+1 por mostrar asyncio.gather (* tarefas). Aqui está um trecho desse tipo que eu usei: urls= [fetch(construct_fetch_url(u),idx) for idx, u in enumerate(some_URI_list)] results = await asyncio.gather(*urls)
Ashwini Kumar
19

Use grequests , é uma combinação de pedidos + módulo Gevent.

O GRequests permite usar solicitações com Gevent para fazer solicitações HTTP assíncronas facilmente.

O uso é simples:

import grequests

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

Crie um conjunto de solicitações não enviadas:

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

Envie todos eles ao mesmo tempo:

>>> grequests.map(rs)
[<Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>]
Akshay Pratap Singh
fonte
7
Agora o gevent suporta python 3
Benjamin Toueg
14
grequests não faz parte das solicitações normais e parece ser em grande parte unmaintaned
Thom
8

Uma boa abordagem para solucionar esse problema é primeiro escrever o código necessário para obter um resultado e incorporar o código de segmentação para paralelizar o aplicativo.

Em um mundo perfeito, isso significaria simplesmente iniciar simultaneamente 100.000 threads que produzem seus resultados em um dicionário ou lista para processamento posterior, mas na prática você está limitado em quantas solicitações HTTP paralelas você pode emitir dessa maneira. Localmente, você tem limites em quantos soquetes pode abrir simultaneamente, quantos threads de execução o seu interpretador Python permitirá. Remotamente, você pode estar limitado no número de conexões simultâneas se todas as solicitações forem contra um servidor ou muitos. Essas limitações provavelmente exigirão que você escreva o script de forma a pesquisar apenas uma pequena fração dos URLs a qualquer momento (100, como outro pôster mencionado, é provavelmente um tamanho decente do conjunto de encadeamentos, embora você possa achar que pode implantar com sucesso muito mais).

Você pode seguir este padrão de design para resolver o problema acima:

  1. Inicie um encadeamento que inicie novos encadeamentos de solicitação até que o número de encadeamentos em execução no momento (você pode rastreá-los via threading.active_count () ou empurrando os objetos de encadeamento para uma estrutura de dados) seja> = seu número máximo de solicitações simultâneas (digamos 100) , depois dorme por um curto tempo limite. Esse encadeamento deve terminar quando não houver mais URLs para processar. Assim, o encadeamento continuará acordando, iniciando novos encadeamentos e dormindo até que você termine.
  2. Faça com que os threads de solicitação armazenem seus resultados em alguma estrutura de dados para recuperação e saída posteriores. Se a estrutura na qual você está armazenando os resultados for um listou dictno CPython, você poderá anexar ou inserir com segurança itens exclusivos de seus encadeamentos sem bloqueios , mas se gravar em um arquivo ou precisar de uma interação de dados entre encadeamentos mais complexa, use um bloqueio de exclusão mútua para proteger esse estado da corrupção .

Eu sugiro que você use o módulo de segmentação . Você pode usá-lo para iniciar e rastrear threads em execução. O suporte de encadeamento do Python é simples, mas a descrição do seu problema sugere que ele é completamente suficiente para suas necessidades.

Finalmente, se você gostaria de ver uma aplicação direta bonita de um aplicativo de rede paralela escrito em Python, veja ssh.py . É uma pequena biblioteca que usa o Python threading para paralelizar muitas conexões SSH. O design está próximo o suficiente de seus requisitos, para que você possa ser um bom recurso.

Erik Garrison
fonte
1
erikg: jogar uma fila na sua equação seria razoável (para bloqueio de exclusão mútua)? Suspeito que o GIL do Python não esteja voltado para brincar com milhares de threads.
IgorGanapolsky
Por que você precisa de bloqueio de exclusão mútua para impedir a geração de muitos threads? Eu suspeito que não entendi o termo. Você pode acompanhar os threads em execução em uma fila de threads, removendo-os quando concluídos e adicionando mais ao referido limite de threads. Mas em um caso simples como o em questão, você também pode apenas observar o número de threads ativos no processo atual do Python, esperar até que ele fique abaixo de um limite e iniciar mais threads até o limite, conforme descrito. Eu acho que você poderia considerar isso um bloqueio implícito, mas nenhum bloqueio explícito é necessário.
Erik Garrison
erikg: vários threads não compartilham estado? Na página 305 do livro de O'Reilly, "Python para Unix e Linux System Administration", afirma: "... o uso de threading sem filas o torna mais complexo do que muitas pessoas conseguem lidar realisticamente. É uma idéia muito melhor usar sempre o enfileiramento Se você acha que precisa usar threads. Por quê? Porque o módulo de fila também alivia a necessidade de proteger explicitamente os dados com mutexes, porque a própria fila já está protegida internamente por um mutex. " Mais uma vez, congratulo-me com o seu ponto de vista sobre isso.
IgorGanapolsky
Igor: Você está absolutamente certo de que deve usar uma fechadura. Eu editei a postagem para refletir isso. Dito isso, a experiência prática com python sugere que você não precisa bloquear estruturas de dados que você modifica atomicamente de seus threads, como list.append ou a adição de uma chave de hash. O motivo, acredito, é o GIL, que fornece operações como list.append com um grau de atomicidade. No momento, estou executando um teste para verificar isso (use 10k threads para acrescentar números 0-9999 a uma lista, verifique se todos os anexos funcionaram). Após quase 100 iterações, o teste não falhou.
Erik Garrison
Igor: Fiz outra pergunta sobre este tópico: stackoverflow.com/questions/2740435/…
Erik Garrison
7

Se você deseja obter o melhor desempenho possível, considere usar E / S assíncrona em vez de threads. A sobrecarga associada a milhares de threads do sistema operacional não é trivial e a alternância de contexto no interpretador Python acrescenta ainda mais. O encadeamento certamente fará o trabalho, mas suspeito que uma rota assíncrona fornecerá melhor desempenho geral.

Especificamente, eu sugeriria o cliente da Web assíncrono na biblioteca Twisted ( http://www.twistedmatrix.com ). Ele tem uma curva de aprendizado reconhecidamente íngreme, mas é bastante fácil de usar quando você conhece bem o estilo de programação assíncrona do Twisted.

Um API de cliente da Web assíncrono do HowTo na Twisted está disponível em:

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

Rakis
fonte
Rakis: Atualmente, estou analisando E / S assíncronas e sem bloqueio. Preciso aprender melhor antes de implementá-lo. Um comentário que gostaria de fazer em sua postagem é que é impossível (pelo menos na minha distribuição Linux) gerar "milhares de threads de SO". Existe um número máximo de threads que o Python permitirá que você gere antes que o programa seja interrompido. E, no meu caso (no CentOS 5), o número máximo de threads é 303.
IgorGanapolsky
É bom saber disso. Eu nunca tentei gerar mais de um punhado de Python de uma só vez, mas eu esperava poder criar mais do que isso antes de bombardear.
Rakis
6

Uma solução:

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()

Hora do teste:

[kalmi@ubi1:~] wc -l urllist.txt
10000 urllist.txt
[kalmi@ubi1:~] time python f.py > /dev/null 

real    1m10.682s
user    0m16.020s
sys 0m10.330s
[kalmi@ubi1:~] 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
[kalmi@ubi1:~] 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
Tarnay Kálmán
fonte
6
Usar o Twisted como um pool de threads está ignorando a maioria dos benefícios que você pode obter dele. Você deve usar o cliente HTTP assíncrono.
Jean-Paul Calderone
1

Usar um pool de threads é uma boa opção e facilitará bastante isso. Infelizmente, o python não possui uma biblioteca padrão que torna os pools de threads ultra fáceis. Mas aqui está uma biblioteca decente que deve ajudar você a começar: http://www.chrisarndt.de/projects/threadpool/

Exemplo de código do site:

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

Espero que isto ajude.

Kevin Wiskia
fonte
Sugiro que você especifique q_size para ThreadPool assim: ThreadPool (poolsize, q_size = 1000) Para que você não tenha 100000 objetos WorkRequest na memória. "Se q_size> 0, o tamanho da fila de solicitações de trabalho for limitado e o conjunto de encadeamentos bloquear quando a fila estiver cheia e tentar colocar mais solicitações de trabalho nela (consulte o putRequestmétodo), a menos que você também use um timeoutvalor positivo putRequest."
Tarnay Kálmán
Até agora, estou tentando implementar a solução de pool de threads - como sugerido. No entanto, não entendo a lista de parâmetros na função makeRequests. O que é some_callable, list_of_args, callback? Talvez se eu visse um trecho de código real que ajudaria. Estou surpreso que o autor dessa biblioteca não tenha postado QUALQUER exemplo.
IgorGanapolsky
some_callable é a sua função na qual todo o seu trabalho é realizado (conexão ao servidor http). list_of_args são argumentos que serão passados ​​para some_callabe. retorno de chamada é uma função que será chamada quando o segmento de trabalho for concluído. São necessários dois argumentos, o objeto do trabalhador (não precisa se preocupar com isso realmente) e os resultados que o trabalhador recuperou.
precisa saber é o seguinte
1

Criar epollobjeto,
abertos muitos sockets cliente TCP,
ajustar seus buffers de envio a ser um pouco mais de cabeçalho de solicitação,
envie um cabeçalho de solicitação - deve ser imediata, apenas colocar em um buffer, cadastre-se tomada no epollobjeto,
fazer .pollem epollobect,
li pela primeira vez 3 bytes de cada soquete .poll,
escreva-os para sys.stdoutseguidos de\n (não libere), feche o soquete do cliente.

Número limite de soquetes abertos simultaneamente - lida com erros quando os soquetes são criados. Crie um novo soquete somente se outro estiver fechado.
Ajuste os limites do SO.
Tente entrar em alguns processos (não muitos): isso pode ajudar a usar a CPU com um pouco mais de eficiência.

George Sovetov
fonte
@IgorGanapolsky Deve ser. Eu ficaria surpreso caso contrário. Mas certamente precisa de experimentação.
George Sovetov
0

No seu caso, o encadeamento provavelmente fará o truque, pois você provavelmente estará gastando mais tempo aguardando uma resposta. Existem módulos úteis como a fila na biblioteca padrão que podem ajudar.

Eu fiz uma coisa semelhante com o download paralelo de arquivos antes e foi bom o suficiente para mim, mas não estava na escala que você está falando.

Se sua tarefa foi mais ligada à CPU, convém examinar o módulo de multiprocessamento , que permitirá utilizar mais CPUs / núcleos / threads (mais processos que não se bloqueiam, uma vez que o bloqueio é por processo)

Mattias Nilsson
fonte
A única coisa que eu gostaria de mencionar é que gerar vários processos pode ser mais caro do que gerar vários threads. Além disso, não há um ganho de desempenho claro no envio de 100.000 solicitações HTTP com vários processos versus vários encadeamentos.
IgorGanapolsky
0

Considere usar o moinho de vento , embora o Windmill provavelmente não possa fazer tantos tópicos.

Você poderia fazer isso com um script Python rolado manualmente em 5 máquinas, cada uma conectando a saída usando as portas 40000-60000, abrindo 100.000 conexões de porta.

Além disso, pode ajudar a fazer um teste de amostra com um aplicativo de controle de qualidade bem segmentado, como o OpenSTA , para ter uma idéia de quanto cada servidor pode suportar.

Além disso, tente usar o Perl simples com a classe LWP :: ConnCache. Você provavelmente obterá mais desempenho (mais conexões) dessa maneira.

djangofan
fonte
0

Esse cliente da Web assíncrono distorcido é muito rápido.

#!/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)
Robᵩ
fonte
0

Descobri que usar o tornadopacote é a maneira mais rápida e simples de conseguir isso:

from tornado import ioloop, httpclient, gen


def main(urls):
    """
    Asynchronously download the HTML contents of a list of URLs.
    :param urls: A list of URLs to download.
    :return: List of response objects, one for each URL.
    """

    @gen.coroutine
    def fetch_and_handle():
        httpclient.AsyncHTTPClient.configure(None, defaults=dict(user_agent='MyUserAgent'))
        http_client = httpclient.AsyncHTTPClient()
        waiter = gen.WaitIterator(*[http_client.fetch(url, raise_error=False, method='HEAD')
                                    for url in urls])
        results = []
        # Wait for the jobs to complete
        while not waiter.done():
            try:
                response = yield waiter.next()
            except httpclient.HTTPError as e:
                print(f'Non-200 HTTP response returned: {e}')
                continue
            except Exception as e:
                print(f'An unexpected error occurred querying: {e}')
                continue
            else:
                print(f'URL \'{response.request.url}\' has status code <{response.code}>')
                results.append(response)
        return results

    loop = ioloop.IOLoop.current()
    web_pages = loop.run_sync(fetch_and_handle)

    return web_pages

my_urls = ['url1.com', 'url2.com', 'url100000.com']
responses = main(my_urls)
print(responses[0])
RDRR
fonte
-2

A maneira mais fácil seria usar a biblioteca de threads embutida do Python. Eles não são threads "reais" / do kernel. Eles têm problemas (como serialização), mas são bons o suficiente. Você deseja um pool de filas e threads. Uma opção está aqui , mas é trivial escrever sua própria. Você não pode paralelizar todas as 100.000 chamadas, mas pode disparar 100 (aproximadamente) delas ao mesmo tempo.

pestilence669
fonte
7
Os threads do Python são bastante reais, ao contrário do Ruby, por exemplo. Sob o capô, eles são implementados como threads nativos do SO, pelo menos no Unix / Linux e Windows. Talvez você está se referindo a GIL, mas não faz os fios menos real ...
Eli Bendersky
2
Eli está certo sobre os threads do Python, mas o argumento da Pestilence de que você deseja usar um pool de threads também está correto. A última coisa que você gostaria de fazer neste caso é tentar iniciar um encadeamento separado para cada uma das 100 mil solicitações simultaneamente.
Adam Crossland
1
Igor, você não pode postar trechos de código sensatos nos comentários, mas pode editar sua pergunta e adicioná-los lá.
Adam Crossland
Pestilência: quantas filas e threads por fila você recomendaria para minha solução?
IgorGanapolsky
mais isso é uma tarefa I / O bound não vinculado à CPU, a GIL afeta em grande parte as tarefas ligadas CPU
PirateApp