python
Python BeautifulSoup Scraping를 최적화하여 파워 업: 초보자 가이드

Python BeautifulSoup Scraping을 최적화하여 파워 업: 초보자 가이드

MoeNagy Dev

더 빠른 웹 스크래핑을 위한 Beautiful Soup 최적화

Beautiful Soup의 기본 이해

Beautiful Soup은 HTML 및 XML 문서를 파싱하는 간단한 방법을 제공하는 강력한 Python 라이브러리입니다. 이를 사용하면 웹 페이지의 구조를 탐색, 검색 및 수정할 수 있습니다. Beautiful Soup을 사용하려면 라이브러리를 설치하고 Python 스크립트에서 가져와야 합니다:

from bs4 import BeautifulSoup

라이브러리를 import한 후에는 BeautifulSoup 생성자를 사용하여 HTML 문서를 파싱할 수 있습니다:

html_doc = """
<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title"><b>The Dormouse's story</b></p>
<p class="story">일전에 세 어린 자매가 있었습니다. 그들의 이름은
<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> 및
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>였으며,
그들은 우물의 바닥에 살았습니다.</p>
<p class="story">...</p>
"""
 
soup = BeautifulSoup(html_doc, 'html.parser')

이 예제에서는 'html.parser' 파서를 사용하여 html_doc 문자열에서 BeautifulSoup 객체를 생성합니다. 이 파서는 기본 Python HTML 파서입니다. 그러나 필요에 따라 'lxml' 또는 'lxml-xml'과 같은 다른 파서도 사용할 수 있습니다.

성능 병목 현상 식별하기

Beautiful Soup은 강력한 도구이지만, HTML 파싱은 특히 크거나 복잡한 웹 페이지를 처리할 때 계산적으로 많은 작업이 필요한 작업임을 이해하는 것이 중요합니다. Beautiful Soup 코드의 성능 병목 현상을 식별하는 것은 성능 최적화의 첫 번째 단계입니다.

Beautiful Soup의 성능 문제 중 하나는 HTML 문서를 파싱하는 데 걸리는 시간입니다. 이는 HTML의 크기, 문서 구조의 복잡성 및 사용한 파싱 모드 등의 요인에 의해 영향을 받을 수 있습니다.

다른 잠재적인 병목 현상은 파싱된 HTML 트리를 검색하고 탐색하는 데 소요되는 시간입니다. 쿼리의 복잡성 및 HTML 문서의 크기에 따라 이 과정도 시간이 오래 걸릴 수 있습니다.

Beautiful Soup 코드에서 성능 병목 현상을 식별하려면 Python의 내장 timeit 모듈이나 cProfile과 같은 프로파일링 도구를 사용할 수 있습니다. 다음은 HTML 문서를 파싱하는 데 걸리는 시간을 측정하는 timeit을 사용한 예입니다:

import timeit
 
setup = """
from bs4 import BeautifulSoup
html_doc = '''
<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title"><b>The Dormouse's story</b></p>
<p class="story">일전에 세 어린 자매가 있었습니다. 그들의 이름은
<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> 및
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>였으며,
그들은 우물의 바닥에 살았습니다.</p>
<p class="story">...</p>
'''
"""
 
stmt = """
soup = BeautifulSoup(html_doc, 'html.parser')
"""
 
print(timeit.timeit(stmt, setup=setup, number=1000))

이 코드에서는 BeautifulSoup 파싱 작업을 1,000번 실행하고 평균 실행 시간을 보고합니다. 이와 비슷한 기술을 사용하여 Beautiful Soup 코드의 다른 부분(예: HTML 트리 검색 및 탐색)의 성능을 측정할 수 있습니다.

Beautiful Soup 성능 향상을 위한 전략

Beautiful Soup 코드의 성능 병목 현상을 식별한 후, 성능을 향상시키기 위한 전략을 구현할 수 있습니다. 일반적인 전략 몇 가지를 소개합니다:

  1. HTML 파싱 최적화: 사용 사례에 맞는 최적의 파싱 모드를 선택합니다. Beautiful Soup은 'html.parser', 'lxml', 'lxml-xml'을 비롯한 여러 파싱 모드를 지원합니다. 각 모드에는 각각의 장단점이 있으므로 특정 HTML 구조에 가장 적합한 모드를 테스트하여 찾아야 합니다.

    # 'lxml' 파서 사용 예
    soup = BeautifulSoup(html_doc, 'lxml')
  2. 병렬 처리 활용: 큰 HTML 문서를 처리하거나 여러 웹 스크래핑 작업을 수행할 때 Beautiful Soup은 느릴 수 있습니다. 이런 경우 작업을 병렬화하기 위해 멀티스레딩이나 병렬 처리를 사용하여 작업 속도를 높일 수 있습니다.

    import threading
     
    def scrape_page(url):
        response = requests.get(url)
        soup = BeautifulSoup(response.content, 'html.parser')
        # soup 객체 처리
        # ...
     
    urls = ['https://example.com/page1', 'https://example.com/page2', ...]
    threads = []
     
    for url in urls:
            thread = threading.Thread(target=scrape_page, args=(url,))
            thread.start()
            threads.append(thread)
     
    for thread in threads:
        thread.join()
  3. 캐싱 및 메모이제이션 구현: 이전 웹 스크래핑 작업의 결과를 캐싱하는 것은 성능을 크게 향상시킬 수 있습니다. 특히 동일한 웹사이트를 반복적으로 스크래핑하는 경우 이 기법을 사용하여 Beautiful Soup 코드의 반복 계산을 최적화할 수 있습니다.

    from functools import lru_cache
     
    @lru_cache(maxsize=128)
    def scrape_page(url):
        response = requests.get(url)
        soup = BeautifulSoup(response.content, 'html.parser')
        # soup 객체 처리
        # ...
        return result
  4. Pandas 및 NumPy와 통합: 표 형식 데이터와 함께 작업하는 경우 Beautiful Soup을 Pandas 및 NumPy와 통합하여 효율적인 데이터 조작 기능을 활용할 수 있습니다. 이를 통해 웹 스크래핑 작업의 성능을 크게 향상시킬 수 있습니다.

   import pandas as pd
   from bs4 import BeautifulSoup
 
   html_doc = """
   <table>
       <tr>
           <th>이름</th>
           <th>나이</th>
           <th>도시</th>
       </tr>
       <tr>
           <td>John</td>
           <td>30</td>
           <td>New York</td>
       </tr>
       <tr>
           <td>Jane</td>
           <td>25</td>
           <td>Los Angeles</td>
       </tr>
   </table>
   """
 
   soup = BeautifulSoup(html_doc, 'html.parser')
   table = soup.find('table')
   rows = table.find_all('tr')
 
   data = []
   for row in rows[1:]:
       cols = row.find_all('td')
       name = cols[0].text
       age = int(cols[1].text)
       city = cols[2].text
       data.append({'이름': name, '나이': age, '도시': city})
 
   df = pd.DataFrame(data)
   print(df)
이 예제에서 `add_numbers()` 함수는 `a`와 `b`라는 두 개의 인수를 받고 그 합계를 반환합니다.
 
함수는 여러 개의 매개변수를 가질 수 있으며, 이러한 매개변수에 대한 기본값을 정의할 수도 있습니다:
 
```python
def greet(name, message="Hello"):
    print(f"{message}, {name}!")
 
greet("Bob")  # 출력: Hello, Bob!
greet("Alice", "Hi")  # 출력: Hi, Alice!

이 예제에서 greet() 함수는 namemessage라는 두 개의 매개변수를 가지지만, message는 기본값으로 "Hello"를 가집니다. 함수를 하나의 인수로 호출하면 message의 기본값을 사용합니다.

함수는 다른 함수 내에서 정의될 수도 있으며, 이렇게 하면 중첩된 함수가 생성됩니다. 이를 로컬 함수 또는 내부 함수라고 합니다. 다음은 예시입니다:

def outer_function(x):
    print(f"{x}와 함께 outer_function 실행")
 
    def inner_function(y):
        print(f"{y}와 함께 inner_function 실행")
        return x + y
 
    result = inner_function(5)
    return result
 
output = outer_function(3)
print(output)  # 출력: 8

이 예제에서 inner_function()outer_function() 내에서 정의되었습니다. inner_function()inner_function()의 매개변수가 아니더라도 outer_function()x 매개변수에 접근할 수 있습니다.

모듈과 패키지

Python에서 코드를 모듈패키지로 구성하여 더 관리 가능하고 재사용 가능하게 할 수 있습니다.

모듈은 정의 및 문장을 포함하는 단일 Python 파일입니다. 모듈을 코드에 가져와서 그들이 정의하는 함수, 클래스 및 변수를 사용할 수 있습니다. 다음은 예시입니다:

# math_utils.py
def add(a, b):
    return a + b
 
def subtract(a, b):
    return a - b
# main.py
import math_utils
 
result = math_utils.add(5, 3)
print(result)  # 출력: 8

이 예제에서는 math_utils.py라는 모듈이 있으며, add()subtract()라는 두 개의 함수가 정의되어 있습니다. main.py 파일에서 math_utils 모듈을 가져와 제공하는 함수를 사용합니다.

패키지는 관련된 모듈의 모음입니다. 패키지는 디렉터리와 하위 디렉터리로 구성된 계층적인 구조로 구성됩니다. 다음은 예시입니다:

my_package/
    __init__.py
    math/
        __init__.py
        utils.py
    text/
        __init__.py
        formatting.py

이 예제에서 my_packagemathtext라는 두 개의 서브 패키지를 포함하는 패키지입니다. 각 디렉터리에는 패키지로 인식하기 위해 필요한 __init__.py 파일이 있습니다.

점 표기법을 사용하여 패키지에서 모듈을 가져올 수 있습니다:

from my_package.math.utils import add
from my_package.text.formatting import format_text
 
result = add(5, 3)
formatted_text = format_text("Hello, world!")

이 예제에서는 math 서브패키지의 utils.py 모듈에서 add() 함수를 가져오고, text 서브패키지의 formatting.py 모듈에서 format_text() 함수를 가져옵니다.

예외

예외는 Python 코드에서 오류와 예기치 않은 상황을 다루는 방법입니다. 예외가 발생하면 프로그램의 일반적인 흐름이 중단되고, 인터프리터는 적절한 예외 처리기를 찾으려고 합니다.

예외 처리 예시입니다:

try:
    result = 10 / 0
except ZeroDivisionError:
    print("오류: 0으로 나누기")

이 예제에서는 10을 0으로 나누려고 하면 ZeroDivisionError가 발생합니다. except 블록에서 이 예외를 잡아내고 오류 메시지를 출력합니다.

단일 try-except 블록에서 여러 개의 예외를 처리할 수도 있습니다:

try:
    x = int(input("숫자를 입력하세요: "))
    y = 10 / x
except ValueError:
    print("오류: 잘못된 입력")
except ZeroDivisionError:
    print("오류: 0으로 나누기")

이 예제에서는 먼저 사용자의 입력을 정수로 변환하려고 시도합니다. 입력이 잘못된 경우 ValueError가 발생하며, 첫 번째 except 블록에서 이 예외를 잡아냅니다. 입력이 유효하지만 사용자가 0을 입력하면 ZeroDivisionError가 발생하며, 두 번째 except 블록에서 이 예외를 잡아냅니다.

또한 기본 Exception 클래스 또는 이 클래스의 하위 클래스에서 상속받은 클래스를 만들어 사용자 정의 예외를 정의할 수도 있습니다:

class CustomException(Exception):
    pass
 
def divide(a, b):
    if b == 0:
        raise CustomException("오류: 0으로 나누기")
    return a / b
 
try:
    result = divide(10, 0)
except CustomException as e:
    print(e)

이 예제에서는 CustomException이라는 사용자 정의 예외를 정의하고, divide() 함수에 0으로 나누는 분모가 주어지면 이 예외를 발생시킵니다. 그런 다음 이 예외를 try-except 블록에서 잡아내고 오류 메시지를 출력합니다.

결론

이 튜토리얼에서는 함수, 모듈, 패키지 및 예외와 같은 다양한 Python의 고급 개념에 대해 알아보았습니다. 이러한 기능은 더 복잡하고 구조화된 Python 코드를 작성하는 데 필수적입니다.

함수는 로직을 캡슐화하고 재사용할 수 있도록 해줌으로써 코드를 더 모듈화하고 유지 관리 가능하게 만듭니다. 모듈과 패키지는 코드를 논리적 단위로 구성하여 관리 및 공유를 쉽게 할 수 있게 도와줍니다. 예외는 오류와 예기치 않은 상황을 처리할 수 있는 방법을 제공하여 프로그램이 실행 중에 발생할 수 있는 문제를 원활하게 처리할 수 있게 합니다.

이러한 개념을 숙달하면 견고하고 확장 가능한 애플리케이션을 구축할 수 있는 능력 있는 Python 개발자로 나아가는 데 도움이 될 것입니다.