본문 바로가기
IT/Scraping

웹 크롤링 모델: 객체 계획 및 정의와 다양한 웹사이트 레이아웃 다루기

by Cyber_ 2025. 2. 14.

웹 크롤러의 응용방향은 거의 끝이 없지만, 확장성이 뛰어난 크롤러는 일정한 패턴 중 하나에 속하는 경우가 많습니다.
다양한 웹사이트에서 식당 리뷰, 뉴스 기사, 회사 프로필 같은 한정된 '타입'의 데이터를 수집하고, 이 데이터 타입을 파이썬 객체에 저장해서 데이터베이스에 읽고 쓰는 웹 크롤러에 대해 학습해보려고 합니다.

객체 계획 및 정의

각 제품에 다음과 같은 필드가 있다면

  • 제품 이름
  • 가격
  • 설명
  • 사이즈
  • 색깔
  • 옷감 종류
  • 고객 평가

이 모든 정보를 추적할 필요는 없습니다. 정답은 무엇을 하고싶은 가를 살펴봐야 합니다. 만약 여러 매장의 제품 가격을 비교하고 시간에 따라 해당 제품 가격이 어떻게 변하는지 추적하고 싶다면

  • 품명
  • 제조사
  • 제품 ID

와 같이 고유하게 식별할 수 있는 정보면 충분할 것입니다. 중요한 것은

  • 분석에 필요한 데이터 속성
  • 다른 사이트에서도 공통되게 사용되는 속성

을 구분해 내는 것입니다.

 

데이터 모델은 모든 코드의 기초가 됩니다. 모델을 잘못 결정하면 코드를 작성하고 유지 관리하기 어려워지거나, 결과 데이터를 추출하고 효율적으로 사용하기 어려워질 수 있습니다. 수집할 웹사이트가 다양할수록 무엇을 수집하고 어떻게 저장할지 진지하게 생각하고 정확하게 계획하는 것이 중요합니다.

 

다양한 웹사이트 레이아웃 다루기

구글 같은 검색엔진의 가장 인상적인 업적 중 하나는 그 구조에 대한 지식도 없이 다양한 웹사이트에서 관련성 있고 유용한 데이터를 추출하는 것입니다.

적은 정보를 통해 유의미한 데이터를 추출하는 방법 중 가장 확실한 것은 각 웹사이트에 대해 별도의 웹 크롤러 또는 페이지 구문 분석기를 만드는 것입니다. 각각은 URL, 문자열 또는 BeautifulSoup 객체를 받아, 스크랩한 내용을 파이썬 객체로 반환할 수입니다. 다음은 뉴스 기사 같은 웹사이트 콘텐츠에 대응하는 Content 클래스와 BeautifulSoup 객체를 받아 Content 인스턴스를 반환하는 스크레이퍼 함수 두개로 구성된 에제 입니다.

import requests
from bs4 import BeautifulSoup

class Content:
    def __init__(self, url, title, body):
        self.url = url
        self.title = title
        self.body = body

def getPage(url):
    req = requests.get(url)
    return BeautifulSoup(req.text, 'html.parser')

def scrapeNYTimes(url):
    bs = getPage(url)
    title = bs.find('h1').text
    lines = bs.select('div.StoryBodyCompanionColumn div p')
    body = '\n'.join([line.text for line in lines])
    return Content(url, title, body)

def scrapeBrookings(url):
    bs = getPage(url)
    title = bs.find('h1').text
    body = bs.find('div', {'class', 'post-body'}).text
    return Content(url,title, body)

url = '''
https://www.brookings.edu/blog/future-development/2018/01/26/
delivering-inclusive-urban-access-3-uncomfortable-truths/
'''

content = scrapeBrookings(url)
print('Title: {}'.format(content.title))
print('URL: {}\n'.format(content.url))
print(content.body)

url = '''
https://www.nytimes.com/2018/01/25/
opinion/sunday/silicon-valley-immortality.html
'''
content = scrapeNYTimes(url)
print('Title: {}'.format(content.title))
print('URL: {}'.format(content.url))
print(content.body)

 

여기서 사용한 변수들 중 사이트에 따라 달라지는 변수는 각 정보를 얻는데 사용한 CSS 선택자 뿐입니다. BeautifulSOup의 find, find_all 삼수는 매개변수로 태그 문자열과 키/값 속성으로 이루어진 딕셔너리를 받습니다. 즉, 사이트 구조와 데이터 위치를 정의하는 매개 변수를 넘기면 됩니다.

 

수집할 정보에 대응하는 CSS 선택자를 각각 문자열 하나로 만들고, 이들을 딕셔너리 객체에 모아서 BeautifulSoup select 함수와 함께 사용하면 더욱 편리합니다.

 

class Content:
    """
    글/페이지 전체에 사용할 기반 클래스
    """
    def __init__(self, url, title, body):
        self.url = url
        self.title = title
        self.body = body

    def print(self)
        """
        출력 결과를 원하는 대로 바꿀 수 있는 함수
           """
        print("URL: {}".format(self.url))
        print("Title: {}".format(self.title))
        print("Body: {}".format(self.body))

class Website:
    """
    웹사이트 구조에 관한 정보를 저장할 클래스
    """
    def __init__(self, name, url, titleTag, bodyTag):
        self.name = name
        self.url = url
        self.titleTag = titleTag
        self.bodyTag = bodyTag

 

Content와 Website 클래스를 사용하면 주어진 웹 페이지에 존재하는 URL의 제목과 내용을 모두 스크랩할 Crawler를 작성할 수 있습니다.

 

import requests
from bs4 import BeautifulSoup

class Crawler:
    def getPage(self, url):
        try:
            req = requests.get(url)
        except requests.exceptions.RequestException:
            return None
        return BeautifulSoup(req.text, 'html.parser')

    def safeGet(self, pageObj, selector):
        """
        BeautifulSoup 객체와 선택자를 받아 콘텐츠 문자열을 추출하는 함수
        주어진 선택자로 검색된 결과가 없다면 빈 문자열을 반환합니다.
        """

        selectedElems = pageObj.select(selector)
        if selectedElems is not None and len(selectedElems) > 0:
            return '\n'.join(
            [elem.get_text() for elem in selectedElems])
        return ''

    def parse(self, site, url):
        """
        URL을 받아 콘첸트를 추출합니다.
        """
        bs = self.getPage(url)
        if bs is not None:
            title = self.safeGet(bs, site.titleTag)
            body = self.safeGet(bs, site.bodyTag)
            if title != ''and body != '':
                content = Content(url, title, body)
                content.print()

 

 

다음은 다양한 웹사이트 객체를 정의하고 프로세스를 시작하는 코드입니다.

 

crawler = Crawler()

siteData = [
    ['O\'Reilly Media', 'http://oreilly.com',
        'h1', 'section#product-description'],
    ['Reuters', 'http://reuters.com',
        'h1','div.StandardArticleBOdy_body_1gnLA']
    ['Brookings', 'http://www.brookings.edu',
        'h1', 'div.post-body']
]

website = []
urls = [
    'http://shop.orilly.com/product/0636920028154.do',
    'http://www.reuters.com/article/us-usa-epa-pruitt-idUSKBN19W2D0',
    'https://www.brookings.edu/blog/techtank/2016/03/01/idea-to-retire-old-metods-of-policy-education/'
]
for row in siteData:
    websites.append(Website(row[0], row[1], row[2], row[3]))

crawler.parse(website[0], urls[0])
crawler.parse(website[1], urls[1])
crawler.parse(website[2], urls[2])

 

사이트를 수집해야하는 프로젝트가 늘어날수록 위와 같이 클래스와 해서 사용한다면 엄청난 유연성을 확보할 수 있을 것입니다.