어두운 proxyscrape 로고

동시성 대 병렬성: 웹 스크래핑의 중요한 차이점

스크래핑, 차이점, 1월-01-20225분 읽기

When it comes to concurrency vs. parallelism, it may be apparent as they refer to the same concepts in executions of computer programs in a multi-threaded environment. Well, after looking at their definitions in the Oxford dictionary, you may be inclined to think so. However, when you go deeper into these notions with respect to

동시성과 병렬성은 멀티 스레드 환경에서 컴퓨터 프로그램을 실행할 때 동일한 개념을 지칭하는 것이 분명할 수 있습니다. 옥스퍼드 사전의 정의를 보고 나면 그렇게 생각할 수도 있습니다. 그러나 CPU가 프로그램 명령을 실행하는 방식과 관련하여 이러한 개념을 자세히 살펴보면 동시성과 병렬성이 서로 다른 개념이라는 것을 알 수 있습니다. 

이 글에서는 동시성과 병렬성이 어떻게 다른지, 그리고 프로그램 실행 생산성을 향상시키기 위해 어떻게 함께 작동하는지에 대해 자세히 살펴봅니다. 마지막으로 웹 스크래핑에 가장 적합한 두 가지 전략에 대해 설명합니다. 그럼 시작해 보겠습니다.

동시 실행이란 무엇인가요?

먼저 간단하게 설명하기 위해 단일 프로세서에서 실행되는 단일 애플리케이션의 동시성부터 시작하겠습니다. Dictionary.com에서는 동시성을 결합된 동작 또는 노력과 동시 이벤트의 발생으로 정의합니다. 그러나 병렬 실행에 대해서도 실행이 일치한다고 말할 수 있으므로 컴퓨터 프로그래밍의 세계에서는 이 정의가 다소 오해의 소지가 있습니다.

일상 생활에서는 컴퓨터에서 여러 작업을 동시에 실행할 수 있습니다. 예를 들어, 브라우저에서 블로그 기사를 읽으면서 Windows Media Player에서 음악을 들을 수 있습니다. 다른 웹 페이지에서 PDF 파일을 다운로드하는 또 다른 프로세스가 실행 중일 수도 있는데, 이 모든 예는 별도의 프로세스입니다.

동시 실행 애플리케이션이 발명되기 전에는 CPU가 순차적으로 프로그램을 실행했습니다. 이는 한 프로그램의 명령이 실행을 완료해야 CPU가 다음 프로그램으로 이동한다는 것을 의미했습니다.

반면 동시 실행은 모든 프로세스가 완료될 때까지 각 프로세스를 조금씩 번갈아 가며 실행합니다.

단일 프로세서 다중 스레드 실행 환경에서는 다른 프로그램이 사용자 입력이 차단되면 한 프로그램이 실행됩니다. 이제 멀티 스레드 환경이 무엇인지 궁금하실 것입니다. 멀티 스레드는 서로 독립적으로 실행되는 스레드의 모음으로, 다음 섹션에서 스레드에 대해 자세히 설명합니다.

동시성을 병렬 실행과 혼동해서는 안 됩니다.

이제 동시성과 병렬성을 혼동하기 쉽습니다. 위의 예에서 동시성이 의미하는 바는 프로세스가 병렬로 실행되지 않는다는 것입니다. 

대신 한 프로세스가 입력/출력 작업을 완료해야 한다고 가정하면, 운영 체제는 해당 프로세스가 입출력 작업을 완료하는 동안 다른 프로세스에 CPU를 할당합니다. 이 절차는 모든 프로세스가 실행을 완료할 때까지 계속됩니다.

그러나 운영 체제에 의한 작업 전환은 나노 또는 마이크로초 이내에 이루어지기 때문에 사용자에게는 프로세스가 병렬로 실행되는 것처럼 보일 수 있습니다, 

스레드란 무엇인가요?

순차 실행과 달리, 현재 아키텍처에서는 CPU가 전체 프로세스/프로그램을 한 번에 실행하지 않을 수 있습니다. 대신 대부분의 컴퓨터는 전체 프로세스를 임의의 순서로 서로 독립적으로 실행되는 여러 개의 경량 구성 요소로 분할할 수 있습니다. 이러한 경량 구성 요소를 스레드라고 합니다.

예를 들어, Google 문서도구에는 동시에 작동하는 여러 개의 스레드가 있을 수 있습니다. 한 스레드가 작업을 자동으로 저장하는 동안 다른 스레드는 백그라운드에서 실행되어 맞춤법 및 문법을 검사할 수 있습니다.  

운영 체제에서 우선순위를 지정할 스레드의 순서와 우선순위를 결정하며, 이는 시스템에 따라 다릅니다.

병렬 실행이란 무엇인가요?

이제 단일 CPU를 사용하는 환경에서 컴퓨터 프로그램을 실행하는 방법을 알게 되었습니다. 이와는 대조적으로 최신 컴퓨터는 병렬 실행이라고 하는 여러 CPU에서 동시에 많은 프로세스를 실행합니다. 현재 대부분의 아키텍처에는 여러 개의 CPU가 있습니다.

아래 다이어그램에서 볼 수 있듯이 CPU는 프로세스에 속한 각 스레드를 서로 평행하게 실행합니다.  

병렬 처리에서 운영 체제는 시스템 아키텍처에 따라 매크로 또는 마이크로초 단위로 CPU와 스레드를 전환합니다. 운영 체제가 병렬 실행을 달성하기 위해 컴퓨터 프로그래머는 병렬 프로그래밍이라는 개념을 사용합니다. 병렬 프로그래밍에서 프로그래머는 여러 CPU를 최대한 활용할 수 있도록 코드를 개발합니다. 

동시성으로 웹 스크래핑 속도를 높이는 방법

웹 스크래핑을 이용해 웹사이트에서 데이터를 스크랩하는 도메인이 너무 많기 때문에 방대한 양의 데이터를 스크랩하는 데 많은 시간이 소요된다는 단점이 있습니다. 숙련된 개발자가 아니라면 결국 오류 없이 완벽하게 코드를 실행하기까지 특정 기술을 실험하는 데 많은 시간을 낭비하게 될 수 있습니다.

아래 섹션에서는 웹 스크래핑이 느린 이유에 대해 간략하게 설명합니다.

웹 스크래핑이 느린 중요한 이유는 무엇인가요?

먼저, 스크래퍼는 웹 스크래핑에서 대상 웹사이트로 이동해야 합니다. 그런 다음 스크래핑하려는 HTML 태그에서 엔티티를 가져와서 검색해야 합니다. 마지막으로, 대부분의 경우 데이터를 CSV 형식과 같은 외부 파일에 저장합니다.  

보시다시피 위의 작업은 대부분 웹사이트에서 데이터를 가져온 다음 외부 파일에 저장하는 것과 같은 무거운 바인딩 I/O 작업을 필요로 합니다. 대상 웹사이트로 이동하는 데는 네트워크 속도나 네트워크가 연결될 때까지의 대기 시간 등 외부 요인에 따라 달라지는 경우가 많습니다.

아래 그림에서 볼 수 있듯이, 세 개 이상의 웹사이트를 스크랩해야 하는 경우 이렇게 극도로 느린 시간 소모는 스크래핑 프로세스에 추가적인 장애를 초래할 수 있습니다. 이 그림은 스크래핑 작업을 순차적으로 수행한다고 가정합니다.

따라서 어떤 식으로든 스크래핑 작업에 동시성 또는 병렬성을 적용해야 합니다. 다음 섹션에서 병렬 처리에 대해 먼저 살펴보겠습니다.

Python을 사용한 웹 스크래핑의 동시성

지금쯤이면 동시성과 병렬성에 대한 개요를 이해하셨을 것입니다. 이 섹션에서는 Python의 간단한 코딩 예제를 통해 웹 스크래핑의 동시성에 대해 집중적으로 살펴보겠습니다.

동시 실행 없이 시연하는 간단한 예시

이 예에서는 Wikipedia의 인구를 기준으로 수도 목록으로 국가의 URL을 스크랩하겠습니다. 이 프로그램은 링크를 저장한 다음 240개 페이지 각각으로 이동하여 해당 페이지의 HTML을 로컬에 저장합니다.

 동시성의 효과를 보여드리기 위해 순차적으로 실행되는 프로그램과 멀티 스레드로 동시에 실행되는 프로그램 두 가지를 보여드리겠습니다.

코드는 다음과 같습니다:

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

코드 설명

먼저 HTML 데이터를 추출하기 위해 BeautifulSoap을 포함한 라이브러리를 가져옵니다. 다른 라이브러리에는 웹사이트에 액세스하기 위한 요청, URL을 연결하기 위한 urllib, 프로그램의 총 실행 시간을 알아내기 위한 시간 라이브러리가 포함됩니다.

요청 가져오기
bs4에서 BeautifulSoup를 가져옵니다.
urllib.parse에서 urljoin을 가져옵니다.
가져오기 시간

프로그램은 먼저 get_countries() 함수를 호출하는 메인 모듈로 시작합니다. 그런 다음 이 함수는 HTML 파서를 통해 BeautifulSoup 인스턴스를 통해 countries 변수에 지정된 Wikipedia URL에 액세스합니다.

그런 다음 앵커 태그의 href 속성에서 값을 추출하여 표에 있는 국가 목록의 URL을 검색합니다.

검색하는 링크는 상대 링크입니다. urljoin 함수는 이를 절대 링크로 변환합니다. 그런 다음 이러한 링크는 all_countries 배열에 추가되어 기본 함수로 반환됩니다. 

그런 다음 가져오기 함수는 각 링크의 HTML 콘텐츠를 HTML 파일로 저장합니다. 이것이 바로 이 코드 조각들이 하는 일입니다:

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

마지막으로, 메인 기능은 파일을 저장하는 데 걸린 시간을 HTML 형식으로 인쇄합니다. 저희 PC에서는 131.22초가 걸렸습니다.

이 시간은 확실히 더 빨라질 수 있다. 다음 섹션에서 동일한 프로그램을 여러 스레드로 실행하는 방법을 알아보겠습니다.

동시성이 있는 동일한 프로그램

멀티 스레드 버전에서는 프로그램이 더 빠르게 실행되도록 사소한 변경 사항을 조정해야 합니다.

동시성은 여러 개의 스레드를 생성하고 프로그램을 실행하는 것임을 기억하세요. 스레드를 생성하는 방법에는 수동으로 생성하는 방법과 ThreadPoolExecutor 클래스를 사용하는 방법 두 가지가 있습니다. 

수동으로 스레드를 생성한 후 모든 스레드에 조인 함수를 사용하여 수동 방법을 사용할 수 있습니다. 이렇게 하면 메인 메서드는 모든 스레드가 실행을 완료할 때까지 기다립니다.

이 프로그램에서는 concurrent. futures 모듈의 일부인 ThreadPoolExecutor 클래스로 코드를 실행할 것입니다. 따라서 우선 위 프로그램에 아래 줄을 넣어야 합니다. 

concurrent.futures에서 ThreadPoolExecutor를 가져옵니다.

그런 다음 HTML 콘텐츠를 HTML 형식으로 저장하는 for 루프를 다음과 같이 변경할 수 있습니다:

  실행자로 ThreadPoolExecutor(max_workers=32)를 사용합니다:
           executor.map(fetch, clinks)

위의 코드는 최대 32개의 스레드가 있는 스레드 풀을 생성합니다. CPU마다 최대 작업자 매개변수가 다르므로 다양한 값으로 실험해봐야 합니다. 스레드 수가 많을수록 실행 시간이 빨라지는 것과 반드시 일치하는 것은 아닙니다.

따라서 우리 PC에서는 15.14초의 출력을 생성했는데, 이는 순차적으로 실행했을 때보다 훨씬 나은 결과입니다.

다음 섹션으로 넘어가기 전에 동시 실행 프로그램의 최종 코드는 다음과 같습니다:

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

병렬 처리로 웹 스크래핑 속도를 높이는 방법

이제 동시 실행에 대해 이해하셨기를 바랍니다. 더 나은 분석을 위해 동일한 프로그램이 여러 CPU에서 병렬로 실행되는 멀티프로세서 환경에서 어떻게 수행되는지 살펴보겠습니다.

먼저 필요한 모듈을 가져와야 합니다:

멀티프로세싱에서 풀,cpu_count 가져오기

파이썬은 컴퓨터의 CPU 수를 계산하는 cpu_count() 메서드를 제공합니다. 이 메서드는 병렬로 수행할 수 있는 작업의 정확한 수를 결정하는 데 도움이 됩니다.

이제 순차 실행에서 for 루프가 있는 코드를 이 코드로 대체해야 합니다:

풀 (cpu_count())을 p로 사용합니다:
 
   p.map(fetch,clinks)

이 코드를 실행한 결과 전체 실행 시간은 20.10초로 첫 번째 프로그램에서 순차적으로 실행한 것보다 상대적으로 빨랐습니다.

결론

이 시점에서 병렬 프로그래밍과 순차 프로그래밍에 대한 포괄적인 개요를 이해하셨기를 바라며, 어느 쪽을 사용할지 선택하는 것은 주로 직면한 특정 시나리오에 따라 달라집니다.

웹 스크래핑 시나리오의 경우, 동시 실행으로 시작한 다음 병렬 솔루션으로 전환하는 것이 좋습니다. 이 글이 도움이 되셨기를 바라며, 블로그에서 이와 같은 웹 스크래핑과 관련된 다른 글도 읽어보시기 바랍니다.