웹 스크래핑 속도 향상 방법: 완벽 가이드

고급 기법을 활용하여 웹 스크래핑 프로세스를 최적화하고 데이터 검색 속도를 높이는 방법을 알아보세요.
10 분 읽기
How to Make Web Scraping Faster

이 글에서는 다음을 살펴보겠습니다:

  • 웹 스크래핑 속도가 느려지는 주요 원인
  • 웹 스크래핑 속도 향상을 위한 다양한 기법
  • 데이터 검색 속도 향상을 위한 샘플 Python 스크래핑 스크립트 최적화 방법

자, 시작해 보겠습니다!

스크래핑 프로세스가 느려지는 이유

웹 스크래핑 프로세스가 느려질 수 있는 주요 원인을 살펴보세요.

원인 #1: 느린 서버 응답

웹 스크래핑 속도에 영향을 미치는 가장 두드러진 요소 중 하나는 서버 응답 시간입니다. 웹사이트에 요청을 보내면 서버가 이를 처리하고 응답합니다. 서버가 느리면 요청 완료 시간이 길어집니다. 서버가 느려지는 이유는 트래픽 과부하, 제한된 리소스, 네트워크 지연 등이 있습니다.

안타깝게도 대상 서버 속도를 높이는 데는 별다른 방법이 없습니다. 이는 여러분의 통제 범위를 벗어난 문제입니다. 단, 지연이 여러분 측의 과도한 요청량 때문이라면 예외입니다. 이 경우 요청 사이에 무작위 지연을 추가하여 요청을 더 긴 시간에 걸쳐 분산시키세요.

이유 #2: 느린 CPU 처리

스크래핑 스크립트의 실행 속도는 CPU 처리 속도에 크게 좌우됩니다. 스크립트를 순차적으로 실행할 경우 CPU는 각 작업을 하나씩 처리해야 하므로 시간이 많이 소요됩니다. 특히 스크립트에 복잡한 계산이나 데이터 변환이 포함된 경우 이 현상이 두드러집니다.

또한 HTML 파싱에는 시간이 소요되며 스크래핑 속도를 크게 저하시킬 수 있습니다. HTML 웹 스크래핑에 관한 저희 글을 참고하세요.

이유 #3: 제한된 I/O 작업

입출력(I/O) 작업은 스크래핑 작업의 병목 현상이 되기 쉽습니다. 대상 사이트가 여러 페이지로 구성되어 있을 때 특히 그렇습니다. 스크립트가 외부 리소스의 응답을 기다린 후 진행하도록 설계된 경우 상당한 지연이 발생할 수 있습니다.

요청을 보내고, 서버의 응답을 기다리고, 이를 처리한 다음 다음 요청으로 넘어가는 방식은 웹 스크래핑을 수행하는 효율적인 방법이 아닙니다.

기타 원인

웹 스크래핑 스크립트 속도를 저하시키는 다른 요인들은 다음과 같습니다:

  • 비효율적인 코드: 최적화되지 않은 스크래핑 로직은 전체 스크래핑 프로세스를 느리게 할 수 있습니다. 비효율적인 데이터 구조, 불필요한 루프, 과도한 로깅을 피하세요.
  • 속도 제한: 대상 사이트가 특정 시간 내에 사용자가 보낼 수 있는 요청 수를 제한하는 경우, 자동 스크레이퍼의 속도가 느려집니다. 해결책? 프록시 서비스!
  • CAPTCHA 및 기타 스크래핑 방지 솔루션: CAPTCHA와 봇 방지 조치는 사용자 상호작용을 요구함으로써 스크래핑 과정을 방해할 수 있습니다. 다른 스크래핑 방지 기법을 알아보세요.

웹 스크래핑 가속화 기법

이 섹션에서는 웹 스크래핑 속도를 높이는 가장 인기 있는 방법을 알아봅니다. 기본적인 Python 스크래핑 스크립트로 시작하여 다양한 최적화가 미치는 영향을 보여드리겠습니다.

참고: 여기서 소개하는 기법은 모든 프로그래밍 언어나 기술에 적용 가능합니다. 파이썬은 단순성을 위해 사용되었으며, 웹 스크래핑에 가장 적합한 프로그래밍 언어 중 하나이기 때문입니다.

다음은 초기 파이썬 스크래핑 스크립트입니다:

import requests
from bs4 import BeautifulSoup
import csv
import time

def scrape_quotes_to_scrape():
    # 스크래핑 대상 페이지 URL 배열
    urls = [
        "http://quotes.toscrape.com/",
        "https://quotes.toscrape.com/page/2/",
        "https://quotes.toscrape.com/page/3/",
        "https://quotes.toscrape.com/page/4/",
        "https://quotes.toscrape.com/page/5/",
        "https://quotes.toscrape.com/page/6/",
        "https://quotes.toscrape.com/page/7/",
        "https://quotes.toscrape.com/page/8/",
        "https://quotes.toscrape.com/page/9/",
        "https://quotes.toscrape.com/page/10/"
    ]

    # 스크랩된 데이터를 저장할 위치
    quotes = []

    # 페이지를 순차적으로 스크래핑
    for url in urls:
        print(f"스크래핑 중인 페이지: '{url}'")

        # 페이지 HTML을 가져오기 위해 GET 요청 전송
        response = requests.get(url)
        # BeautifulSoup을 사용하여 페이지 HTML 파싱
        soup = BeautifulSoup(response.content, "html.parser")

        # 페이지의 모든 인용문 요소 선택
        quote_html_elements = soup.select(".quote")

        # 인용문 요소 반복 처리 및 내용 스크래핑
        for quote_html_element in quote_html_elements:
            # 인용문 텍스트 추출
            text = quote_html_element.select_one(".text").get_text()
            # 인용문의 저자 추출
            author = quote_html_element.select_one(".author").get_text()
            # 인용문과 연관된 태그 추출
            tags = [tag.get_text() for tag in quote_html_element.select(".tag")]

            # 새 quote 객체 생성 및 목록에 추가
            quote = {
                "text": text,
                "author": author,
                "tags": ", ".join(tags)
            }
            quotes.append(quote)

        print(f"페이지 '{url}' 스크래핑 성공n")

    print("스크래핑된 데이터를 CSV로 내보냄")

    # 스크랩한 명언을 CSV 파일로 내보내기
    with open("quotes.csv", "w", newline="", encoding="utf-8") as csvfile:
        fieldnames = ["text", "author", "tags"]
        writer = csv.DictWriter(csvfile, fieldnames=fieldnames)

        writer.writeheader()
        writer.writerows(quotes)

    print("인용문 CSV로 내보냄n")

# 실행 시간 측정
start_time = time.time()
scrape_quotes_to_scrape()
end_time = time.time()

execution_time = end_time - start_time
print(f"실행 시간: {execution_time:.2f} 초")

위 스크레이퍼는 Quotes to Scrape 웹사이트의 10개 페이지별 URL을 대상으로 합니다. 각 URL에 대해 스크립트는 다음 작업을 수행합니다:

  1. requests를 사용하여 페이지의 HTML을 가져오기 위한 GET 요청을 보냅니다.
  2. BeautifulSoup으로 HTML 콘텐츠를 파싱합니다.
  3. 페이지의 각 인용문 요소에서 인용문 텍스트, 저자, 태그를 추출합니다.
  4. 스크랩된 데이터를 사전 목록에 저장합니다.

마지막으로 추출된 데이터를 quotes.csv라는 CSV 파일로 내보냅니다.

스크립트를 실행하려면 필요한 라이브러리를 설치하세요:

pip install requests beautifulsoup4

scrape_quotes_to_scrape() 함수 호출은 스크래핑 과정에 소요되는 시간을 측정하기 위해 time.time() 호출로 감싸져 있습니다. 저희 머신에서 초기 스크립트 실행에는4.51초가 소요됩니다.

스크립트 실행 시 프로젝트 폴더에 quotes.csv 파일이 생성됩니다. 또한 다음과 유사한 로그를 확인할 수 있습니다:

Scraping page: 'http://quotes.toscrape.com/'
Page 'http://quotes.toscrape.com/' scraped successfully

Scraping page: 'https://quotes.toscrape.com/page/2/'
Page 'https://quotes.toscrape.com/page/2/' scraped successfully

페이지 'https://quotes.toscrape.com/page/3/' 스크래핑 중
페이지 'https://quotes.toscrape.com/page/3/' 스크래핑 성공

페이지 'https://quotes.toscrape.com/page/4/' 스크래핑 중
페이지 'https://quotes.toscrape.com/page/4/' 스크래핑 성공

스크래핑 중인 페이지: 'https://quotes.toscrape.com/page/5/'
페이지 'https://quotes.toscrape.com/page/5/' 스크래핑 성공

페이지 'https://quotes.toscrape.com/page/6/' 스크래핑 중
페이지 'https://quotes.toscrape.com/page/6/' 스크래핑 성공

페이지 'https://quotes.toscrape.com/page/7/' 스크래핑 중
페이지 'https://quotes.toscrape.com/page/7/' 스크래핑 성공

스크래핑 중인 페이지: 'https://quotes.toscrape.com/page/8/'
페이지 'https://quotes.toscrape.com/page/8/' 스크래핑 성공

스크래핑 중인 페이지: 'https://quotes.toscrape.com/page/9/'
페이지 'https://quotes.toscrape.com/page/9/' 스크래핑 성공

스크래핑 중인 페이지: 'https://quotes.toscrape.com/page/10/'
페이지 'https://quotes.toscrape.com/page/10/' 스크래핑 성공

스크래핑된 데이터 CSV로 내보내기 중
인용문 CSV로 내보냄

실행 시간: 4.63초

이 출력은 스크립트가 Quotes to Scrape의 각 페이지로 나뉜 웹페이지를 순차적으로 스크래핑함을 명확히 보여줍니다. 곧 보시게 될 것처럼, 몇 가지 최적화를 통해 이 프로세스의 흐름과 속도를 크게 바꿀 수 있습니다.

이제 웹 스크래핑을 더 빠르게 만드는 방법을 알아봅시다!

1. 더 빠른 HTML 파싱 라이브러리 사용

데이터 파싱은 시간과 자원을 소모하며, 다양한 HTML 파서들이 이 작업을 수행하기 위해 서로 다른 접근 방식을 사용합니다. 일부는 자체 설명형 API를 통한 풍부한 기능 세트를 제공하는 데 중점을 두는 반면, 다른 일부는 성능을 우선시합니다. 자세한 내용은 최고의 HTML 파서에 대한 가이드를 확인하세요.

파이썬에서 Beautiful Soup은 가장 인기 있는 HTML 파서이지만, 반드시 가장 빠른 것은 아닙니다. 더 많은 맥락을 위해 벤치마크 결과를 확인해 보세요.

사실 Beautiful Soup은 다양한 기본 파서들을 감싸는 래퍼 역할만 합니다. 초기화 시 두 번째 인수를 통해 원하는 파서를 지정할 수 있습니다:

soup = BeautifulSoup(response.content, "html.parser")

일반적으로 Beautiful Soup은 파이썬 표준 라이브러리의 내장 파서인 html.parser와 함께 사용됩니다. 그러나 속도를 중시한다면 lxml을 고려해야 합니다. C 구현을 기반으로 하기 때문에 파이썬에서 사용할 수 있는 가장 빠른 HTML 파서 중 하나입니다.

lxml을 설치하려면 다음 명령어를 실행하세요:

pip install lxml

설치 후 Beautiful Soup과 함께 다음과 같이 사용할 수 있습니다:

soup = BeautifulSoup(response.content, "lxml")

이제 Python 스크래핑 스크립트를 다시 실행하세요. 이번에는 다음과 같은 출력이 표시될 것입니다:

# 간략화를 위해 생략...

실행 시간: 4.35초

실행 시간이 4.61초에서 4.35초로 단축되었습니다. 이 변화는 작아 보일 수 있지만, 이 최적화의 효과는 파싱되는 HTML 페이지의 크기와 복잡성, 선택되는 요소 수에 크게 좌우됩니다.

이 예시에서 대상 사이트는 단순하고 짧으며 얕은 DOM 구조를 가진 페이지를 가지고 있습니다. 그럼에도 불구하고 작은 코드 변경만으로 약 6%의 속도 향상을 달성한 것은 가치 있는 성과입니다!

👍 장점:

  • Beautiful Soup에서 구현이 용이함

👎 단점:

  • 이점이 작음
  • 복잡한 DOM 구조를 가진 페이지에서는 작동하지 않음
  • 더 빠른 HTML 파서들은 더 복잡한 API를 가질 수 있음

2. 멀티프로세싱 스크래핑 구현

멀티프로세싱은 프로그램이 여러 프로세스를 생성하여 병렬 실행을 수행하는 접근 방식입니다. 각 프로세스는 CPU 코어에서 병렬적이고 독립적으로 작동하여 작업을 순차적으로가 아닌 동시에 수행합니다.

이 방법은 웹 스크래핑과 같은 I/O에 의존하는 작업에 특히 유용합니다. 주된 병목 현상이 웹 서버 응답 대기 시간인 경우가 많기 때문입니다. 다중 프로세스를 활용하면 여러 페이지에 동시에 요청을 전송하여 전체 스크래핑 시간을 단축할 수 있습니다.

스크래핑 스크립트를 멀티프로세싱에 맞게 조정하려면 실행 로직에 몇 가지 중요한 수정이 필요합니다. 아래 단계를 따라 Python 스크래퍼를 순차적 방식에서 멀티프로세싱 방식으로 전환하세요.

Python에서 멀티프로세싱을 시작하려면 먼저 multiprocessing 모듈에서 Pool과 cpu_count를 임포트하세요:

from multiprocessing import Pool, cpu_count

Pool은 작업자 프로세스 풀을 관리하는 데 필요한 기능을 제공합니다. 반면 cpu_count는 병렬 처리에 사용할 수 있는 CPU 코어 수를 확인하는 데 도움이 됩니다.

다음으로, 단일 URL을 스크래핑하는 로직을 함수 내에 분리합니다:

def scrape_page(url):
    print(f"Scraping page: '{url}'")

    response = requests.get(url)
    soup = BeautifulSoup(response.content, "html.parser")
    quote_html_elements = soup.select(".quote")

    quotes = []
    for quote_html_element in quote_html_elements:
        text = quote_html_element.select_one(".text").get_text()
        author = quote_html_element.select_one(".author").get_text()
        tags = [tag.get_text() for tag in quote_html_element.select(".tag")]
        quotes.append({
            "text": text,
            "author": author,
            "tags": ", ".join(tags)
        })

    print(f"페이지 '{url}' 스크래핑 성공n")

    return quotes

위 함수는 각 작업자 프로세스에 의해 호출되며 한 번에 하나의 CPU 코어에서 실행됩니다.

그런 다음 순차적 스크래핑 흐름을 멀티프로세싱 로직으로 대체합니다:

def scrape_quotes():
    urls = [
        "http://quotes.toscrape.com/",
        "https://quotes.toscrape.com/page/2/",
        "https://quotes.toscrape.com/page/3/",
        "https://quotes.toscrape.com/page/4/",
        "https://quotes.toscrape.com/page/5/",
        "https://quotes.toscrape.com/page/6/",
        "https://quotes.toscrape.com/page/7/",
        "https://quotes.toscrape.com/page/8/",
        "https://quotes.toscrape.com/page/9/",
        "https://quotes.toscrape.com/page/10/"
    ]

    # 프로세스 풀 생성
    with Pool(processes=cpu_count()) as pool:
        results = pool.map(scrape_page, urls)

    # 결과 리스트 평탄화
    quotes = [quote for sublist in results for quote in sublist]

    print("스크랩된 데이터를 CSV로 내보냄")

    with open("quotes_multiprocessing.csv", "w", newline="", encoding="utf-8") as csvfile:
        fieldnames = ["text", "author", "tags"]
        writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
        writer.writeheader()
        writer.writerows(quotes)

    print("명언 CSV로 내보냄n")

마지막으로, 실행 시간을 측정하면서 scrape_quotes() 함수를 실행합니다:

if __name__ == "__main__":
    start_time = time.time()
    scrape_quotes()
    end_time = time.time()

    execution_time = end_time - start_time
    print(f"실행 시간: {execution_time:.2f} 초")

참고: if __name__ == "__main__": 구문은 모듈이 임포트될 때 코드의 특정 부분이 실행되는 것을 방지하기 위해 필요합니다. 이 확인이 없으면 멀티프로세싱 모듈이 새 프로세스를 생성하려 시도할 수 있으며, 특히 Windows에서 예상치 못한 동작을 유발할 수 있습니다.

모든 것을 합치면 다음과 같습니다:

from multiprocessing import Pool, cpu_count
import requests
from bs4 import BeautifulSoup
import csv
import time

def scrape_page(url):
    print(f"Scraping page: '{url}'")

    response = requests.get(url)
    soup = BeautifulSoup(response.content, "html.parser")
    quote_html_elements = soup.select(".quote")

    quotes = []
    for quote_html_element in quote_html_elements:
        text = quote_html_element.select_one(".text").get_text()
        author = quote_html_element.select_one(".author").get_text()
        tags = [tag.get_text() for tag in quote_html_element.select(".tag")]
        quotes.append({
            "text": text,
            "author": author,
            "tags": ", ".join(tags)
        })

    print(f"페이지 '{url}' 스크래핑 성공했습니다n")

    return quotes

def scrape_quotes():
    urls = [
        "http://quotes.toscrape.com/",
        "https://quotes.toscrape.com/page/2/",
        "https://quotes.toscrape.com/page/3/",
        "https://quotes.toscrape.com/page/4/",
        "https://quotes.toscrape.com/page/5/",
        "https://quotes.toscrape.com/page/6/",
        "https://quotes.toscrape.com/page/7/",
        "https://quotes.toscrape.com/page/8/",
        "https://quotes.toscrape.com/page/9/",
        "https://quotes.toscrape.com/page/10/"
    ]

    # 프로세스 풀 생성
    with Pool(processes=cpu_count()) as pool:
        results = pool.map(scrape_page, urls)

    # 결과 리스트 평탄화
    quotes = [quote for sublist in results for quote in sublist]

    print("스크랩된 데이터를 CSV로 내보냄")

    with open("quotes_multiprocessing.csv", "w", newline="", encoding="utf-8") as csvfile:
        fieldnames = ["text", "author", "tags"]
        writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
        writer.writeheader()
        writer.writerows(quotes)

    print("인용문 CSV로 내보냄")

if __name__ == "__main__":
    start_time = time.time()
    scrape_quotes()
    end_time = time.time()

    execution_time = end_time - start_time
    print(f"실행 시간: {execution_time:.2f} 초")

스크립트를 다시 실행합니다. 이번에는 다음과 같은 로그가 생성됩니다:

페이지 스크래핑: 'http://quotes.toscrape.com/'
페이지 스크래핑: 'https://quotes.toscrape.com/page/2/'
페이지 스크래핑: 'https://quotes.toscrape.com/page/3/'
페이지 스크래핑: 'https://quotes.toscrape.com/page/4/'
페이지 스크래핑: 'https://quotes.toscrape.com/page/5/'
페이지 스크래핑: 'https://quotes.toscrape.com/page/6/'
스크래핑 중인 페이지: 'https://quotes.toscrape.com/page/7/'
스크래핑 중인 페이지: 'https://quotes.toscrape.com/page/8/'
페이지 'http://quotes.toscrape.com/' 스크래핑 성공

스크래핑 중인 페이지: 'https://quotes.toscrape.com/page/9/'
페이지 'https://quotes.toscrape.com/page/3/' 성공적으로 스크래핑됨

스크래핑 중인 페이지: 'https://quotes.toscrape.com/page/10/'
페이지 'https://quotes.toscrape.com/page/4/' 스크래핑 성공

페이지 'https://quotes.toscrape.com/page/5/' 스크래핑 성공

페이지 'https://quotes.toscrape.com/page/6/' 스크래핑 성공

페이지 'https://quotes.toscrape.com/page/7/' 스크래핑 성공

페이지 'https://quotes.toscrape.com/page/2/' 스크래핑 성공

페이지 'https://quotes.toscrape.com/page/8/' 스크래핑 성공

페이지 'https://quotes.toscrape.com/page/9/' 스크래핑 성공

페이지 'https://quotes.toscrape.com/page/10/' 스크래핑 성공

스크래핑된 데이터를 CSV로 내보냄
인용문 CSV로 내보냄

실행 시간: 1.87초

보시다시피 실행 순서가 더 이상 순차적이지 않습니다. 이제 스크립트가 여러 페이지를 동시에 스크래핑할 수 있습니다. 구체적으로 CPU에 사용 가능한 코어 수(이 경우 8개)만큼 동시에 처리할 수 있습니다.

병렬 처리로 인해 실행 시간이 약 145% 단축되어 4.61초에서 1.87초로 감소했습니다. 정말 인상적이네요!

👍 장점:

  • 뛰어난 실행 시간 향상
  • 대부분의 프로그래밍 언어에서 기본 지원

👎 단점:

  • 사용 중인 머신의 코어 수에 제한됨
  • URL 목록의 순서를 따르지 않음
  • 코드를 많이 수정해야 함

3. 멀티스레딩 스크래핑 구현

멀티스레딩은 단일 프로세스 내에서 여러 스레드를 동시에 실행하는 프로그래밍 기법입니다. 이를 통해 각 작업이 전용 스레드로 처리되면서 스크립트가 여러 작업을 동시에 수행할 수 있습니다.

멀티프로세싱과 유사하지만, 멀티스레딩은 반드시 여러 CPU 코어를 필요로 하지 않습니다. 이는 단일 CPU 코어가 동일한 메모리 공간을 공유하며 동시에 수많은 스레드를 실행할 수 있기 때문입니다. 이 개념에 대해서는 동시성 대 병렬 처리 가이드에서 자세히 알아보세요.

스크래핑 스크립트를 순차적 접근 방식에서 멀티스레드 방식으로 전환하려면 이전 장에서 설명한 것과 유사한 변경이 필요하다는 점을 명심하세요.

이 구현에서는 Python concurrent.futures 모듈의 ThreadPoolExecutor를 사용할 것입니다. 아래와 같이 임포트할 수 있습니다:

from concurrent.futures import ThreadPoolExecutor

ThreadPoolExecutor는 스레드 풀을 관리하고 이를 동시 실행하도록 해주는 고수준 인터페이스를 제공합니다.

이전과 마찬가지로, 먼저 단일 URL 스크래핑 로직을 함수로 분리합니다(이전 장에서 수행한 방식과 동일). 핵심 차이점은 이제 ThreadPoolExecutor를 활용하여 함수를 다중 스레드로 실행해야 한다는 점입니다:

quotes = []

# 최대 10개의 작업자를 가진 스레드 풀 생성
with ThreadPoolExecutor(max_workers=10) as executor:
    # map을 사용하여 각 URL에 scrape_page 함수 적용
    results = executor.map(scrape_page, urls)

# 모든 스레드의 결과 결합
for result in results:
    quotes.extend(result)

기본적으로 max_workers가 None이거나 지정되지 않은 경우, 시스템의 프로세서 수에 5를 곱한 값이 기본값으로 사용됩니다. 이 경우 스크랩할 페이지가 10개뿐이므로 10으로 설정해도 무방합니다. 너무 많은 스레드를 열면 시스템 속도가 느려지고 성능 향상이 아닌 저하로 이어질 수 있다는 점을 잊지 마십시오.

전체 스크래핑 스크립트는 다음과 같은 코드로 구성됩니다:

from concurrent.futures import ThreadPoolExecutor
import requests
from bs4 import BeautifulSoup
import csv
import time

def scrape_page(url):
    print(f"Scraping page: '{url}'")

    response = requests.get(url)
    soup = BeautifulSoup(response.content, "html.parser")
    quote_html_elements = soup.select(".quote")

    quotes = []
    for quote_html_element in quote_html_elements:
        text = quote_html_element.select_one(".text").get_text()
        author = quote_html_element.select_one(".author").get_text()
        tags = [tag.get_text() for tag in quote_html_element.select(".tag")]
        quotes.append({
            "text": text,
            "author": author,
            "tags": ", ".join(tags)
        })

    print(f"페이지 '{url}' 스크래핑 성공했습니다n")

    return quotes

def scrape_quotes():
    urls = [
        "http://quotes.toscrape.com/",
        "https://quotes.toscrape.com/page/2/",
        "https://quotes.toscrape.com/page/3/",
        "https://quotes.toscrape.com/page/4/",
        "https://quotes.toscrape.com/page/5/",
        "https://quotes.toscrape.com/page/6/",
        "https://quotes.toscrape.com/page/7/",
        "https://quotes.toscrape.com/page/8/",
        "https://quotes.toscrape.com/page/9/",
        "https://quotes.toscrape.com/page/10/"
    ]
    
    # 스크랩된 데이터를 저장할 위치
    quotes = []

    # 최대 10개의 작업자를 가진 스레드 풀 생성
    with ThreadPoolExecutor(max_workers=10) as executor:
        # map을 사용하여 각 URL에 scrape_page 함수 적용
        results = executor.map(scrape_page, urls)

    # 모든 스레드의 결과 결합
    for result in results:
        quotes.extend(result)

    print("스크랩된 데이터를 CSV로 내보냄")

    with open("quotes_multiprocessing.csv", "w", newline="", encoding="utf-8") as csvfile:
        fieldnames = ["text", "author", "tags"]
        writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
        writer.writeheader()
        writer.writerows(quotes)

    print("명언 CSV로 내보냄")

if __name__ == "__main__":
    start_time = time.time()
    scrape_quotes()
    end_time = time.time()

    execution_time = end_time - start_time
    print(f"실행 시간: {execution_time:.2f} 초")

실행하면 아래와 같은 메시지가 기록됩니다:

Scraping page: 'http://quotes.toscrape.com/'
Scraping page: 'https://quotes.toscrape.com/page/2/'
Scraping page: 'https://quotes.toscrape.com/page/3/'
Scraping page: 'https://quotes.toscrape.com/page/4/'
페이지 스크래핑 중: 'https://quotes.toscrape.com/page/5/'
페이지 스크래핑 중: 'https://quotes.toscrape.com/page/6/'
페이지 스크래핑 중: 'https://quotes.toscrape.com/page/7/'
스크래핑 중인 페이지: 'https://quotes.toscrape.com/page/8/'
스크래핑 중인 페이지: 'https://quotes.toscrape.com/page/9/'
스크래핑 중인 페이지: 'https://quotes.toscrape.com/page/10/'
페이지 'http://quotes.toscrape.com/' 성공적으로 스크래핑됨

페이지 'https://quotes.toscrape.com/page/6/' 성공적으로 스크래핑됨

페이지 'https://quotes.toscrape.com/page/7/' 성공적으로 스크래핑됨

페이지 'https://quotes.toscrape.com/page/10/' 스크래핑 성공

페이지 'https://quotes.toscrape.com/page/8/' 스크래핑 성공

페이지 'https://quotes.toscrape.com/page/5/' 스크래핑 성공

페이지 'https://quotes.toscrape.com/page/9/' 스크랩 성공

페이지 'https://quotes.toscrape.com/page/4/' 스크랩 성공

페이지 'https://quotes.toscrape.com/page/3/' 스크랩 성공

페이지 'https://quotes.toscrape.com/page/2/' 스크래핑 성공

스크래핑된 데이터 CSV로 내보내기 중
인용문 CSV로 내보냄

실행 시간: 0.52초

다중 프로세스 스크래핑과 마찬가지로 페이지 실행 순서가 더 이상 순차적이지 않습니다. 이번에는 다중 프로세싱보다 성능 향상이 훨씬 큽니다. 이는 스크립트가 이제 10개의 요청을 동시에 실행할 수 있어 이전 제한(CPU 코어 수인 8개 요청)을 초과했기 때문입니다.

시간 개선 폭이 엄청납니다. 4.61초에서 0.52초로 단축되어 약 885%의 감소율을 보입니다!

👍 장점:

  • 실행 시간의 엄청난 개선
  • 대부분의 기술에서 기본 지원

👎 단점:

  • 적절한 스레드 수 찾기가 쉽지 않음
  • URL 목록의 순서를 따르지 않음
  • 코드를 많이 수정해야 함

4. 비동기 스크래핑 사용

비동기 프로그래밍은 비차단 코드를 작성할 수 있게 해주는 현대적인 프로그래밍 패러다임입니다. 개발자가 멀티스레딩이나 멀티프로세싱을 명시적으로 관리하지 않고도 동시 작업을 처리할 수 있도록 하는 것이 핵심 개념입니다.

전통적인 동기식 접근 방식에서는 각 작업이 종료되어야만 다음 작업이 시작됩니다. 이는 특히 웹 스크래핑과 같은 I/O에 의존하는 작업에서 비효율성을 초래할 수 있습니다. 비동기 프로그래밍을 사용하면 여러 I/O 작업을 동시에 시작하고 완료될 때까지 기다릴 수 있습니다. 이를 통해 스크립트의 응답성과 효율성을 유지할 수 있습니다.

파이썬에서 비동기 스크래핑은 일반적으로 표준 라이브러리의 asyncio 모듈을 사용해 구현됩니다. 이 패키지는 asyncawait 키워드를 통해 코루틴을 활용하여 단일 스레드 동시 코드를 작성할 수 있는 기반을 제공합니다.

그러나 requests와 같은 표준 HTTP 라이브러리는 비동기 작업을 지원하지 않습니다. 따라서 asyncio와 원활하게 연동되도록 특별히 설계된 AIOHTTP 같은 비동기 HTTP 클라이언트를 사용해야 합니다. 이 조합을 통해 스크립트 실행을 차단하지 않고도 여러 HTTP 요청을 동시에 보낼 수 있습니다.

다음 명령어로 AIOHTTP를 설치하세요:

pip install aiohttp

그런 다음 asyncioaiohttp를 임포트하세요:

import asyncio
import aiohttp

이전 장과 마찬가지로 단일 URL 스크래핑 로직을 함수로 캡슐화합니다. 다만 이번에는 함수가 비동기적으로 동작합니다:

async def scrape_url(session, url):
    async with session.get(url) as response:
        print(f"Scraping page: '{url}'")

        html_content = await response.text()
        soup = BeautifulSoup(html_content, "html.parser")
        # 스크래핑 로직...

웹페이지의 HTML을 가져오기 위해 await 함수를 사용한 점에 유의하세요.

함수를 병렬로 실행하려면 AIOHTTP 세션을 생성하고 여러 스크래핑 작업을 수집하세요:

# 스크래핑 작업 동시 실행
async with aiohttp.ClientSession() as session:
    tasks = [scrape_url(session, url) for url in urls]
    results = await asyncio.gather(*tasks)

# 결과 리스트 평탄화
quotes = [quote for sublist in results for quote in sublist]

마지막으로 asyncio.run() 을 사용하여 비동기 메인 스크래핑 함수를 실행합니다:

if __name__ == "__main__":
    start_time = time.time()
    asyncio.run(scrape_quotes())
    end_time = time.time()

    execution_time = end_time - start_time
    print(f"실행 시간: {execution_time:.2f} 초")

Python의 비동기 스크래핑 스크립트에는 다음과 같은 코드 줄이 포함됩니다:

import asyncio
import aiohttp
from bs4 import BeautifulSoup
import csv
import time

async def scrape_url(session, url):
    async with session.get(url) as response:
        print(f"Scraping page: '{url}'")

        html_content = await response.text()
        soup = BeautifulSoup(html_content, "html.parser")
        quote_html_elements = soup.select(".quote")

        quotes = []
        for quote_html_element in quote_html_elements:
            text = quote_html_element.select_one(".text").get_text()
            author = quote_html_element.select_one(".author").get_text()
            tags = [tag.get_text() for tag in quote_html_element.select(".tag")]
            quotes.append({
                "text": text,
                "author": author,
                "tags": ", ".join(tags)
            })

        print(f"페이지 '{url}' 스크래핑 성공했습니다n")

        return quotes

async def scrape_quotes():
    urls = [
        "http://quotes.toscrape.com/",
        "https://quotes.toscrape.com/page/2/",
        "https://quotes.toscrape.com/page/3/",
        "https://quotes.toscrape.com/page/4/",
        "https://quotes.toscrape.com/page/5/",
        "https://quotes.toscrape.com/page/6/",
        "https://quotes.toscrape.com/page/7/",
        "https://quotes.toscrape.com/page/8/",
        "https://quotes.toscrape.com/page/9/",
        "https://quotes.toscrape.com/page/10/"
    ]

    # 스크래핑 작업 동시 실행
    async with aiohttp.ClientSession() as session:
        tasks = [scrape_url(session, url) for url in urls]
        results = await asyncio.gather(*tasks)

    # 결과 리스트 평탄화
    quotes = [quote for sublist in results for quote in sublist]

    print("스크래핑된 데이터를 CSV로 내보냄")

    with open("quotes_multiprocessing.csv", "w", newline="", encoding="utf-8") as csvfile:
        fieldnames = ["text", "author", "tags"]
        writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
        writer.writeheader()
        writer.writerows(quotes)

    print("명언 CSV로 내보냄")

if __name__ == "__main__":
    start_time = time.time()
    asyncio.run(scrape_quotes())
    end_time = time.time()

    execution_time = end_time - start_time
    print(f"실행 시간: {execution_time:.2f} 초")

실행하면 다음과 같은 출력이 나옵니다:

Scraping page: 'http://quotes.toscrape.com/'
Page 'http://quotes.toscrape.com/' scraped successfully                                                                

Scraping page: 'https://quotes.toscrape.com/page/3/'
페이지 'https://quotes.toscrape.com/page/7/' 스크래핑 중
페이지 'https://quotes.toscrape.com/page/9/' 스크래핑 중
페이지 'https://quotes.toscrape.com/page/6/' 스크래핑 중
스크래핑 중인 페이지: 'https://quotes.toscrape.com/page/8/'
스크래핑 중인 페이지: 'https://quotes.toscrape.com/page/10/'
페이지 'https://quotes.toscrape.com/page/3/' 성공적으로 스크래핑됨

페이지 'https://quotes.toscrape.com/page/5/' 스크래핑 중
페이지 'https://quotes.toscrape.com/page/4/' 스크래핑 중
페이지 'https://quotes.toscrape.com/page/7/' 스크래핑 성공

페이지 'https://quotes.toscrape.com/page/9/' 스크래핑 성공

페이지 'https://quotes.toscrape.com/page/6/' 스크래핑 성공

스크래핑 중인 페이지: 'https://quotes.toscrape.com/page/2/'
페이지 'https://quotes.toscrape.com/page/10/' 스크래핑 성공

페이지 'https://quotes.toscrape.com/page/5/' 스크래핑 성공

페이지 'https://quotes.toscrape.com/page/4/' 스크래핑 성공

페이지 'https://quotes.toscrape.com/page/8/' 스크래핑 성공

페이지 'https://quotes.toscrape.com/page/2/' 스크래핑 성공

스크래핑된 데이터를 CSV로 내보냄
인용문 CSV로 내보냄

실행 시간: 0.51초

실행 시간은 멀티스레딩 방식과 유사하지만, 스레드를 수동으로 관리할 필요가 없다는 추가 이점이 있습니다.

👍 장점:

  • 실행 시간 대폭 단축
  • 현대 프로그래밍은 비동기 로직을 기반으로 함
  • 스레드나 프로세스의 수동 관리 불필요

👎 단점:

  • 숙달하기 쉽지 않음
  • URL 목록의 순서를 따르지 않음
  • 전용 비동기 라이브러리가 필요함

5. 웹 스크래핑 속도 향상 팁 및 접근법

웹 스크래핑 속도를 높이는 다른 방법은 다음과 같습니다:

  • 요청 속도 최적화: 요청 간격을 미세 조정하여 속도와 속도 제한 또는 차단 방지 사이의 최적 균형을 찾습니다.
  • 프록시 로테이션: 로테이션 프록시를 사용하여 요청을 여러 IP 주소에 분산시켜 차단될 가능성을 줄이고 더 빠른 스크래핑을 가능하게 합니다. 최고의 로테이션 프록시를 참조하세요.
  • 분산 시스템을 통한 병렬 스크래핑: 스크래핑 작업을 여러 온라인 머신에 분산합니다.
  • 자바스크립트 렌더링 감소: 브라우저 자동화 도구 사용을 피하고, HTML 파서로 HTTP 클라이언트 같은 도구를 선호하세요. 브라우저는 많은 리소스를 소모하며 대부분의 기존 HTML 파서보다 훨씬 느리다는 점을 기억하세요.

결론

이 가이드에서는 웹 스크래핑 속도를 높이는 방법을 살펴보았습니다. 스크래핑 스크립트가 느려지는 주요 원인을 규명하고, 샘플 Python 스크립트를 활용해 이러한 문제를 해결하는 다양한 기법을 검토했습니다. 스크래핑 로직을 약간만 조정해도 실행 시간을 8배 단축할 수 있었습니다.

데이터 수집 속도를 높이기 위해 웹 스크래핑 로직을 수동으로 최적화하는 것이 중요하지만, 적절한 도구를 사용하는 것 또한 동등하게 중요합니다. 브라우저 자동화 솔루션이 필요한 동적 사이트를 대상으로 할 때는 브라우저가 느리고 리소스 집약적이기 때문에 상황이 더 복잡해질 수 있습니다.

이러한 문제를 극복하려면 스크래핑 전용으로 설계된 완전 호스팅 클라우드 기반 솔루션인Scraping Browser를 사용해 보세요. Puppeteer, Selenium, Playwright 등 주요 브라우저 자동화 도구와 완벽하게 연동됩니다. CAPTCHA 자동 해결 기능과 1억 5천만 개 이상의 주거용 IP로 구성된 프록시 네트워크를 기반으로 하여 모든 스크래핑 요구사항을 무제한 확장성으로 지원합니다!

지금 가입하여 무료 체험을 시작하세요.