Concorrência vs Paralelismo: Diferenças significativas para Web Scraping

Raspagem, As diferenças, Jan-17-20225 minutos de leitura

Quando se trata de concorrência vs. paralelismo, pode parecer que se referem aos mesmos conceitos na execução de programas de computador num ambiente multithread. Bem, depois de olhar para as suas definições no dicionário Oxford, pode estar inclinado a pensar assim. No entanto, quando se aprofunda estas noções relativamente a

Quando se trata de concorrência vs. paralelismo, pode parecer que se referem aos mesmos conceitos na execução de programas de computador num ambiente multithread. Bem, depois de olhar para as suas definições no dicionário Oxford, pode estar inclinado a pensar assim. No entanto, quando se aprofunda estas noções relativamente à forma como a CPU executa as instruções do programa, nota-se que a concorrência e o paralelismo são dois conceitos distintos. 

Este artigo aprofunda-se na concorrência e no paralelismo, como variam e como funcionam em conjunto para melhorar a produtividade da execução de programas. Por fim, ele discutirá quais são as duas estratégias mais adequadas para a raspagem da Web. Então, vamos começar.

O que é a execução em simultâneo?

Em primeiro lugar, para simplificar as coisas, vamos começar com a simultaneidade numa única aplicação executada num único processador. O Dictionary.com define a simultaneidade como uma ação ou esforço combinado e a ocorrência de eventos simultâneos. No entanto, poder-se-ia dizer o mesmo sobre a execução paralela, uma vez que as execuções coincidem, pelo que esta definição é algo enganadora no mundo da programação informática.

Na vida quotidiana, terá execuções simultâneas no seu computador. Por exemplo, pode ler um artigo de blogue no seu browser enquanto ouve música no seu Windows Media Player. Haveria outro processo em execução: descarregar um ficheiro PDF de outra página Web - todos estes exemplos são processos separados.

Antes da invenção das aplicações de execução simultânea, as CPUs executavam programas sequencialmente. Isso implicava que as instruções de um programa tinham que completar a execução antes que a CPU passasse para o próximo.

Em contrapartida, a execução simultânea alterna um pouco de cada processo até que todos estejam completos.

Num ambiente de execução multithread com um único processador, um programa é executado quando outro está bloqueado para a entrada do utilizador. Agora pode perguntar o que é um ambiente multithread. É uma coleção de threads que são executadas independentemente umas das outras - mais sobre threads na próxima secção.

A simultaneidade não deve ser confundida com a execução paralela

Agora, então, é mais fácil confundir simultaneidade com paralelismo. O que queríamos dizer com simultaneidade nos exemplos acima é que os processos não estão a correr em paralelo. 

Em vez disso, digamos que um processo requer a conclusão de uma operação de entrada/saída, então o sistema operativo atribui a CPU a outro processo enquanto este conclui a sua operação de E/S. Este procedimento continua até que todos os processos concluam a sua execução.

No entanto, uma vez que a comutação das tarefas pelo sistema operativo ocorre num nano ou microssegundo, o utilizador pode pensar que os processos são executados em paralelo, 

O que é um Thread?

Ao contrário do que acontece na execução sequencial, a CPU pode não executar todo o processo/programa de uma só vez com as arquitecturas actuais. Em vez disso, a maioria dos computadores pode dividir o processo inteiro em vários componentes leves que são executados independentemente uns dos outros numa ordem arbitrária. São estes componentes ligeiros que se designam por "threads".

Por exemplo, o Google Docs pode ter vários segmentos que funcionam em simultâneo. Enquanto um thread guarda automaticamente o seu trabalho, outro pode ser executado em segundo plano, verificando a ortografia e a gramática.  

O sistema operativo determina a ordem e a prioridade das threads, o que depende do sistema.

O que é a execução paralela?

Agora já conhece a execução de programas de computador num ambiente com uma única CPU. Em contrapartida, os computadores modernos executam muitos processos simultaneamente em múltiplas CPUs, o que é conhecido como execução paralela. A maioria das arquiteturas atuais possui múltiplas CPUs.

Como se pode ver no diagrama abaixo, a CPU executa cada thread pertencente a um processo em paralelo entre si.  

No paralelismo, o sistema operativo alterna as threads de e para a CPU em intervalos de macro ou microssegundos, dependendo da arquitetura do sistema. Para que o sistema operativo consiga uma execução paralela, os programadores de computador utilizam o conceito conhecido como programação paralela. Na programação paralela, os programadores desenvolvem código para fazer o melhor uso das múltiplas CPUs. 

Como a concorrência pode acelerar a recolha de dados da Web

Com tantos domínios a utilizarem a raspagem da Web para extrair dados de sítios Web, uma desvantagem significativa é o tempo que consome para extrair grandes quantidades de dados. Se não for um programador experiente, pode acabar por perder muito tempo a experimentar técnicas específicas antes de acabar por executar o código sem erros e na perfeição.

A secção seguinte descreve algumas das razões pelas quais a raspagem da Web é lenta.

Razões significativas para a lentidão da raspagem da Web?

Em primeiro lugar, o raspador tem de navegar para o sítio Web de destino na raspagem da Web. Em seguida, tem de extrair e recuperar as entidades das etiquetas HTML a partir das quais se pretende efetuar o scraping. Finalmente, na maioria das circunstâncias, os dados são guardados num ficheiro externo, como o formato CSV.  

Assim, como pode ver, a maioria das tarefas acima requer operações de E/S muito ligadas, como extrair dados de sítios Web e depois guardá-los em ficheiros externos. A navegação para os sítios Web de destino depende frequentemente de factores externos, como a velocidade da rede ou a espera até que uma rede fique disponível.

Como se pode ver na figura abaixo, este consumo de tempo extremamente lento pode prejudicar ainda mais o processo de recolha de dados quando tem de recolher dados de três ou mais sítios Web. Assume-se que a operação de recolha de dados é efectuada sequencialmente.

Por conseguinte, de uma forma ou de outra, terá de aplicar a concorrência ou o paralelismo às suas operações de recolha de dados. O paralelismo será abordado em primeiro lugar na próxima secção.

Concorrência na recolha de dados da Web utilizando Python

Estou certo de que já tem uma visão geral da concorrência e do paralelismo. Esta secção centrar-se-á na concorrência na recolha de dados da Web com um exemplo simples de codificação em Python.

Um exemplo simples de demonstração sem execução em simultâneo

Neste exemplo, vamos extrair da Wikipédia o URL dos países por uma lista de capitais com base na população. O programa guardaria as ligações e depois iria a cada uma das 240 páginas e guardaria o HTML dessas páginas localmente.

 Para demonstrar os efeitos da concorrência, apresentaremos dois programas - um com execução sequencial e outro em simultâneo com multi-threads.

Aqui está o código:

import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin
import time

def get_countries():
    countries = 'https://en.wikipedia.org/wiki/List_of_national_capitals_by_population'
    all_countries = []
    response = requests.get(countries)
    soup = BeautifulSoup(response.text, "html.parser")
    countries_pl = soup.select('th .flagicon+ a')
    for link_pl in countries_pl:
        link = link_pl.get("href")
        link = urljoin(countries, link)
        
        all_countries.append(link)
    return all_countries
  
def fetch(link):
    res = requests.get(link)
    with open(link.split("/")[-1]+".html", "wb") as f:
        f.write(res.content)
  

        
def main():
    clinks = get_countries()
    print(f"Total pages: {len(clinks)}")
    start_time = time.time()
    for link in clinks:
        fetch(link)
 
    duration = time.time() - start_time
    print(f"Downloaded {len(links)} links in {duration} seconds")
main()

Explicação do código

Em primeiro lugar, importamos as bibliotecas, incluindo a BeautifulSoap, para extrair os dados HTML. As outras bibliotecas incluem a request para aceder ao sítio Web, a urllib para juntar os URLs, como irá descobrir, e a biblioteca time para descobrir o tempo total de execução do programa.

importar pedidos
from bs4 import BeautifulSoup
from urllib.parse import urljoin
importar time

O programa começa com o módulo principal, que chama a função get_countries(). A função acede então ao URL da Wikipédia especificado na variável countries através da instância BeautifulSoup através do analisador HTML.

Em seguida, procura o URL para a lista de países na tabela, extraindo o valor no atributo href da etiqueta âncora.

As ligações que recupera são ligações relativas. A função urljoin converte-as em ligações absolutas. Estas hiperligações são então anexadas à matriz all_countries, que é devolvida à função principal 

Em seguida, a função fetch guarda o conteúdo HTML em cada ligação como um ficheiro HTML. É o que estes pedaços de código fazem:

def fetch(link):
    res = requests.get(link)
    with open(link.split("/")[-1]+".html", "wb") as f:
        f.write(res.content)

Por último, a função principal imprime o tempo necessário para guardar os ficheiros em formato HTML. No nosso PC, demorou 131,22 segundos.

Bem, este tempo poderia certamente ser mais rápido. Vamos descobrir isso na próxima secção, onde o mesmo programa é executado com várias threads.

O mesmo programa com concorrência

Na versão multithread, teríamos de fazer pequenas alterações para que o programa fosse executado mais rapidamente.

Lembre-se de que a simultaneidade tem a ver com a criação de vários threads e a execução do programa. Há duas maneiras de criar threads - manualmente e usando a classe ThreadPoolExecutor. 

Depois de criar as threads manualmente, pode utilizar a função join em todas as threads para o método manual. Ao fazê-lo, o método principal aguardaria que todas as threads concluíssem a sua execução.

Neste programa, vamos executar o código com a classe ThreadPoolExecutor, que faz parte do módulo concurrent. futures. Por isso, antes de mais, tens de colocar a linha abaixo no programa acima. 

from concurrent.futures import ThreadPoolExecutor

Depois disso, pode alterar o ciclo for que guarda o conteúdo HTML em formato HTML da seguinte forma:

  com ThreadPoolExecutor(max_workers=32) como executor:
           executor.map(fetch, clinks)

O código acima cria um pool de threads com um máximo de 32 threads. Para cada CPU, o parâmetro max_workers difere, e é necessário experimentar com valores diferentes. Isso não significa necessariamente que quanto maior o número de threads, mais rápido será o tempo de execução.

Assim, o nosso PC produziu um resultado de 15,14 segundos, o que é muito melhor do que quando o executámos sequencialmente.

Portanto, antes de passarmos à secção seguinte, eis o código final do programa com execução simultânea:

import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin
from concurrent.futures import ThreadPoolExecutor
import time

def get_countries():
    countries = 'https://en.wikipedia.org/wiki/List_of_national_capitals_by_population'
    all_countries = []
    response = requests.get(countries)
    soup = BeautifulSoup(response.text, "html.parser")
    countries_pl = soup.select('th .flagicon+ a')
    for link_pl in countries_pl:
        link = link_pl.get("href")
        link = urljoin(countries, link)
        
        all_countries.append(link)
    return all_countries
  
def fetch(link):
    res = requests.get(link)
    with open(link.split("/")[-1]+".html", "wb") as f:
        f.write(res.content)


def main():
  clinks = get_countries()
  print(f"Total pages: {len(clinks)}")
  start_time = time.time()
  

  with ThreadPoolExecutor(max_workers=32) as executor:
           executor.map(fetch, clinks)
        
 
  duration = time.time()-start_time
  print(f"Downloaded {len(clinks)} links in {duration} seconds")
main()

Como o paralelismo pode acelerar a recolha de dados da Web

Agora esperamos que você tenha entendido o que é execução simultânea. Para ajudá-lo a analisar melhor, vamos ver como o mesmo programa funciona em um ambiente multiprocessado com processos executando paralelamente em várias CPUs.

Em primeiro lugar, é necessário importar o módulo necessário :

from multiprocessing import Pool,cpu_count

Python fornece o método cpu_count(), que conta o número de CPUs em sua máquina. É, sem dúvida, útil para determinar o número exato de tarefas que pode executar em paralelo.

Agora tem de substituir o código com o ciclo for em execução sequencial por este código:

com Pool (cpu_count()) como p:
 
   p.map(fetch,clinks)

Depois de executar este código, obteve-se um tempo de execução global de 20,10 segundos, o que é relativamente mais rápido do que a execução sequencial do primeiro programa.

Conclusão

Nesta altura, esperamos que tenha uma visão global da programação paralela e sequencial - a escolha de uma em detrimento da outra depende essencialmente do cenário específico com que se deparou.

Para o cenário de raspagem da Web, recomendamos começar com a execução simultânea e depois passar para uma solução paralela. Esperamos que tenha gostado de ler este artigo. Não se esqueça de ler outros artigos relevantes para a recolha de dados da Web, como este, no nosso blogue.