Ir para o conteúdo

Velocidade de raspagem da Web: processos, threads e assíncronos

Como especialista em web scraping com mais de 5 anos de experiência, vi em primeira mão como scrapers lentos e ineficientes podem impactar severamente os projetos. Mas com as otimizações certas, você pode acelerar seus web scrapers Python em ordens de magnitude.

Neste guia abrangente, compartilharei as técnicas que aprendi para ajudá-lo a aumentar a velocidade de raspagem usando multiprocessamento, multithreading e assíncio.

Diagnosticando os gargalos de desempenho

Pela minha experiência, existem dois culpados principais que prejudicam o desempenho do web scraper:

Tarefas vinculadas a E/S: operações que exigem espera de recursos externos, como fazer solicitações HTTP ou buscar dados de um banco de dados. Essas tarefas bloqueiam a execução do código enquanto aguardam uma resposta.

Tarefas vinculadas à CPU: Operações que exigem amplo poder de processamento, como análise e extração de informações de HTML, conversão de arquivos, processamento de imagens, etc. Essas tarefas maximizam o uso da CPU.

Das duas, as tarefas vinculadas à E/S tendem a causar mais lentidão, pois os scrapers estão constantemente fazendo solicitações e aguardando respostas. Mas as tarefas da CPU, como a análise, também não podem ser ignoradas.

Para avaliar onde está faltando seu raspador, use o recurso integrado do Python timeit módulo para isolar as partes lentas:

import timeit

# Time a request

timeit.timeit(lambda: requests.get("http://example.com"), number=50)
# 31.23 seconds

# Time parsing
timeit.timeit(lambda: parse_html(content), number=50)  
# 22.12 seconds

Isso pode revelar se as operações de E/S, como solicitações ou tarefas de CPU, como análise, estão ocupando a maior parte do tempo.

Estratégias para dimensionar raspadores Python

Depois de identificar os gargalos, aqui estão as melhores estratégias que encontrei para otimizá-los:

Para tarefas vinculadas a E/S:

  • Use asyncio para realizar E/S simultaneamente sem bloquear

Para tarefas vinculadas à CPU:

  • Aproveite o multiprocessamento para paralelizar o trabalho entre núcleos de CPU

Python fornece ferramentas nativas fantásticas para implementar essas abordagens. Vamos discuti-los em detalhes:

Asyncio: simultaneidade para tarefas vinculadas a E/S

Se o seu raspador está constantemente aguardando a conclusão de operações de E/S, como solicitações, o asyncio permite que você elimine esse tempo perdido executando E/S simultaneamente.

Considere este raspador síncrono:

# Synchronous Scraper

import requests
import time

start = time.time()

for _ in range(50):
  requests.get("http://example.com")

end = time.time()  
print(f"Time taken: {end - start:.2f} secs")

# Time taken: 31.14 secs

Demora mais de 30 segundos para concluir 50 solicitações. A maior parte desse tempo é apenas esperar por respostas.

Agora vamos torná-lo assíncrono com asyncio:

# Asyncio Scraper

import asyncio
import httpx
import time

async def asyn_get(url):
  async with httpx.AsyncClient() as client:
    return await client.get(url)

start = time.time()

loop = asyncio.get_event_loop()
tasks = [loop.create_task(asyn_get("http://example.com")) for _ in range(50)]
wait_tasks = asyncio.wait(tasks)
loop.run_until_complete(wait_tasks)

end = time.time()
print(f"Time taken: {end - start:.2f} secs")

# Time taken: 1.14 secs

Usando o asyncio, podemos emitir todas as solicitações simultaneamente, sem esperar. Isso proporciona uma tremenda aceleração para cargas de trabalho pesadas de E/S.

Na minha experiência, aqui estão algumas dicas para usar o assíncio de maneira eficaz:

  • Sempre aguarde chamadas assíncronas com await
  • Use asyncio.gather() para combinar múltiplas tarefas assíncronas
  • Crie tarefas com loop.create_task() em vez de nu async chamadas
  • Quebrar o código de sincronização com asyncio.to_thread()
  • Use bibliotecas assíncronas como httpx para E/S assíncrona

Asyncio funciona muito bem para otimizar scrapers que realizam grandes volumes de operações de E/S. A seguir, vamos discutir como acelerar gargalos de CPU.

Multiprocessamento: paralelizando cargas de trabalho de CPU

Embora o asyncio ajude na E/S, descobri que o multiprocessamento é a maneira mais eficaz de otimizar o desempenho da CPU para análise, processamento de dados e cálculos.

CPUs modernas possuem múltiplos núcleos que permitem execução paralela. Minha máquina atual tem 8 núcleos:

import multiprocessing
print(multiprocessing.cpu_count())

# 8

Para aproveitar todos esses núcleos, podemos usar multiprocessamento para distribuir o trabalho por vários processos Python.

Aqui está um exemplo para comparar o processamento serial versus paralelo:

# Serial Processing

import time
from slugify import slugify

start = time.time()

articles = ["Article One","Article Two",..."Article One Thousand"]

for title in articles:
  slugify(title)

print(f"Serial time: {time.time() - start:.2f} secs")

# Serial time: 5.14 sec

Isso funciona em apenas 1 núcleo. Vamos paralelizar com o multiprocessamento:

# Parallel Processing 

from multiprocessing import Pool
import time
from slugify import slugify

start = time.time()

with Pool(8) as p:
  p.map(slugify, articles)

print(f"Parallel time: {time.time() - start:.2f} secs")

# Parallel time: 1.04 secs

Usando um pool de 8 trabalhadores, conseguimos processar os dados 5x mais rápido, utilizando todos os núcleos de CPU disponíveis!

Alguns gargalos comuns de CPU em scrapers:

  • Analisando documentos HTML/XML
  • Extraindo texto e dados com Regex
  • Codificação/decodificação de mídia raspada
  • Rastreamento e processamento de Sitemaps
  • Compactando dados copiados

O multiprocessamento permite paralelizar facilmente essas tarefas para reduzir significativamente o tempo de processamento.

Combinando Assíncio e Multiprocessamento

Para melhor desempenho, recomendo combinar assíncio e multiprocessamento em seus scrapers.

Aqui está um modelo que funciona muito bem:

  1. Crie uma async_scrape() função que lida com trabalhos vinculados a E/S, como fazer solicitações usando asyncio.

  2. Ligar async_scrape() de um pool de multiprocessamento para executá-lo em paralelo em vários núcleos.

Isso permite maximizar o paralelismo de E/S e CPU!

Aqui está um exemplo:

import asyncio
from multiprocessing import Pool
import httpx
import time

async def async_scrape(urls):

  async with httpx.AsyncClient() as client:

    tasks = [client.get(url) for url in urls]
    results = await asyncio.gather(*tasks)

    # CPU-heavy processing
    for data in results:
      analyze_data(data)

def multiproc_wrapper(urls):
  asyncio.run(async_scrape(urls))

if __name__ == "__main__":

  urls = [# List of urls

  start = time.time()  

  with Pool(8) as p:
    p.map(multiproc_wrapper, batched_urls)

  print(f"Total time: {time.time() - start:.2f} secs")

Agrupamos URLs em grupos e os coletamos simultaneamente com asyncio usando async_scrape()e processe os lotes em paralelo usando um pool de multiprocessamento.

Isso fornece recursos de escalabilidade massivos, otimizando o desempenho de E/S e da CPU.

Comparando opções de escala

Para resumir, aqui está uma visão geral das várias opções de simultaneidade em Python:

AbordagemspeedupCaso de usoDespesas gerais
MultiprocessamentoMuito altoTarefas vinculadas à CPUAlta
multithreadingModeradoTarefas vinculadas a E/SBaixo
assíncronoMuito altoTarefas vinculadas a E/SBaixo

Com base em extensos benchmarking e experiência no mundo real, descobri multiprocessamento e assíncio fornece o melhor desempenho para web scraping.

O multiprocessamento oferece excelente paralelismo para cargas de trabalho vinculadas à CPU com aceleração de 8x a 10x em uma máquina de 8 núcleos.

Enquanto isso, o asyncio fornece manipulação de E/S assíncrona ainda mais rápida – permitindo milhares de solicitações por segundo em um único thread.

Portanto, combinar os dois funciona incrivelmente bem. O Asyncio elimina a espera por E/S, enquanto o multiprocessamento distribui a análise e o processamento de dados em todos os núcleos.

Comparando o desempenho do Assyncio

Para demonstrar o desempenho bruto do asyncio, comparei a raspagem síncrona versus assíncrona de 1,000 URLs em minha máquina:

Síncrono:

1000 URLs scraped sequentially
Total time: 63.412 seconds

assíncrono:

1000 URLs scraped asynchronously 
Total time: 1.224 seconds

Isso é 50x mais rápido para a mesma carga de trabalho!

Na verdade, os benchmarks mostram que o asyncio pode atingir milhares de solicitações por segundo em um único thread.

Aqui está uma tabela de benchmark asyncIO do excelente biblioteca httpx:

QuadroSolicitações/s
assíncrono15,500
evento14,000
Tornado12,500

Como você pode ver, o asyncio fornece uma taxa de transferência incrível para operações de E/S.

Portanto, utilize-o para qualquer fluxo de trabalho com muita E/S, como fazer solicitações simultâneas ou ler arquivos em seus scrapers.

Aproveitando os serviços de raspagem

Agora que você entende técnicas como assíncio e multiprocessamento, pode estar se perguntando – vale a pena construir tudo isso sozinho?

Em muitos casos, eu recomendo considerar um serviço de API de web scraping como RaspadorAPI or Scrapfly.

Esses serviços cuidam de todo o trabalho pesado de dimensionamento e otimização para você. Aqui estão alguns benefícios:

Simultaneidade e velocidade

Serviços como ScraperAPI e Scrapfly otimizaram a infraestrutura projetada para máxima simultaneidade. Basta passar uma lista de URLs e seus sistemas processarão as solicitações em alta velocidade.

Gerenciamento de proxy

Os serviços de scraping fornecem acesso a milhares de proxies para evitar bloqueios e detecção de bots. A configuração e a rotação de proxies são abstraídas.

Novas tentativas e failover

Os serviços repetem automaticamente as solicitações com falha e mudam para novos proxies conforme necessário, garantindo a obtenção de dados.

Escalabilidade na nuvem

As APIs de scraping podem ser dimensionadas instantaneamente para atender à demanda sem nenhum trabalho de engenharia de sua parte.

Portanto, em muitos casos, pode ser preferível aproveitar uma API de scraping desenvolvida especificamente e concentrar seus esforços em outras áreas.

Principais lições

Aqui estão as principais técnicas que abordei para otimizar o desempenho de web scraping em Python:

  • Identifique gargalos: crie um perfil do seu raspador para isolar tarefas lentas de E/S versus tarefas de CPU.

  • Otimize E/S com assíncrono: use bibliotecas assíncronas e assíncronas para eliminar a espera de solicitações.

  • Paralelizar o trabalho da CPU: aproveite o multiprocessamento para distribuir o processamento de dados em todos os núcleos da CPU.

  • Combine-os: Asyncio para E/S e multiprocessamento para CPU funcionam extremamente bem juntos.

  • Considere raspar APIs: Serviços como ScraperAPI e Scrapfly cuidam da otimização para você.

Com essas abordagens, você pode acelerar seus scrapers em ordens de magnitude. Asyncio e multiprocessamento são seus melhores amigos para uma raspagem Python de alto desempenho.

Deixe-me saber se você tem alguma dúvida! Fico sempre feliz em ajudar outros desenvolvedores a implementar essas técnicas de simultaneidade.

Tags:

Junte-se à conversa

O seu endereço de e-mail não será publicado. Os campos obrigatórios são marcados com *