본문 바로가기
IT/Scraping

웹 크롤러 모델: 검색을 통한 사이트 크롤링

by Cyber_ 2025. 2. 14.

웹사이트 레이아웃이 유연하고 수정하기 편하다 해도 스크랩할 링크를 직접 찾아야 한다면 별로 도움이 되지 않습니다. 자동으로 링크를 수집하고 데이터도 검색할 수 있어야 합니다.

 

기본적으로 사용할 수 있는 웹 크롤러 구조는 세가지 정도가 있습니다.

검색을 통한 웹 크롤링

웹사이트에서 키워드나 주제를 검색하고 검색 결과 목록을 수집하는 프로세스가 사이트마다 크게 다르지 않고, 몇 가지 중요한 요점을 파악하면 별 차이가 없다는 것을 알 수 있습니다.

  • 대부분의 사이트에서 http://example.com?search=myTopic처럼 URL의 검색어를 삽입해 검색결과를 얻을 수 있습니다. 이 URL의 첫 번째 부분은 Website 객체의 속성으로 저장할 수 있으며, 그 뒤에 검색어를 연결하는 일은 아주 간단합니다.
  • 검색 결과 페이지는 쉽게 식별할 수 있는 링크 목록 형태로 제공되는 경우가 대부분이며 보통 <span class="result"> 같은 써먹기 편리한 태그로 둘러싸여 있습니다. 이런 태그 형식도 Website 객체의 속성으로 저장할 수 있습니다.
  • 결과 링크는 /articles/page.html 같은 상대 URL 이거나 http://example.com/article/page.html 같은 절대 URL 입니다. 절대 URL이 필요한지 상대 URL이 필요한지 역시 Website 객체의 속성으로 저장할 수 있습니다.
  • 검색 페이지의 URL을 찾고 표준화했다면 이제 문제는 앞 섹션의 예제, 웹 사이트 형태를 알고 있는 상황에서 페이지에서 데이터를 추출하는 것으로 좁혀집니다.
  •  

우선 다음과 같이 Content 클래스와 Website 클래스를 구현합니다.

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

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

class Website:
    """
    웹사이트 구조에 관한 정보를 저장할 클래스
    """
    def __init__(self, name, url, searchUrl, resultListing,
        resultUrl, absoluteUrl, titleTag, bodyTag):
        self.name = name
        self.url = url
        self.searchUrl = searchUrl
        self.resultListing = resultListing
        self.resultUrl = resultUrl
        self.absoluteUrl = absoluteUrl
        self.titleTag = titleTag
        self.bodyTag = bodyTag
  • searchUrl: URL에 검색어를 추가한 경우 검색결과를 어디에서 얻었는지 정의합니다.
  • resultListing: 각 결과에 관한 정보를 담고 있는 '박스'
  • resultUrl: 결과에서 정확한 URL을 추출할 때 사용할 태그 정보입니다.
  • absoluteUrl: 검색 결과가 절대 URL 인지 상대 URL인지 알려주는 불리언 값입니다.

 

 

아래 크롤러에서 핵심은 Website 데이터, 검색어 목록, 웹사이트 전체에 대해 검색어 전체를 검색하는 이중 루프 입니다.

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):
        childObj = pageObj.select(selector)
        if childObj is not None and len(childObj) > 0:
            return childObj[0].get_text()
        return ''

    def search(self, topic, site):
        """
        주어진 검색어로 주어진 웹사이트를 검색해 결과 페이지를 모두 기록합니다.
        """
        bs = self.getPage(site.searchUrl + topic)
        searchResults = bs.select(site.resultListing)
        for result in searchResults:
            url = result.select(site.resultUrl)[0].attrs['href']
            # 상대 URL인지 절대 URL인지 확인합니다.
            if(site.absoluteUrl):
                bs = self.getPage(url)
            else:
                bs = self.getPage(site.url + url)
            if bs is None:
                print('Someting was wrong with that page or URL. Skipping!')
                return
            title = self.safeGet(bs, site.titleTag)
            body = self.safeGet(bs, site.bodyTag)
            if title !+ '' and body != '':
                content = Content(topic, url, title, body)
                content.print()



crawler = Crawler()

siteData = [
    ['O\'Reilly Media', 
    'http://oreilly.com',
    'https://ssearch.oreilly.com/?q=',
    'article.product-result',
    'p.title a',
    True,
    'h1', 
    'section#product-description'],
    ['Reuters', 'http://reuters.com',
     'http://www.reuters.com/search/news?blob=',
     'div.search-result-content',
     'h3.search-result-title a',
     False,
     'h1',           
     'div.StandardArticleBOdy_body_1gnLA']
    ['Brookings', 'http://www.brookings.edu',
     'https://www.brookings.edu/search/?s=',
     'div.list-content article',
     'h4.title a',
     True,
     'h1', 
     'div.post-body']
]

sites = []
for row in siteData:
    sites.append(Website(row[0], row[1], row[2], row[3], row[4], row[5], row[6], row[7]))

topics = ['python', 'data science']
for topic in topics:
    print('GETTING INFO ABOUT: ' + topic)
    for targetSite in sites:
        crawler.search(topic, targetSite)

 

이 스크립트는 topics 리스트의 항목을 모두 반복하되, 각 스크랩을 시작하기 전에 다음과 같이 어떤 주제에 대해 스크랩하는지 알립니다.

GETTING INFO ABOUT python

 

그런 다음 sites 리스트의 사이트를 모두 반복하여, 외부 루프에서 지정한 검색어로 각 사이트를 스크랩합니다. 페이지에 대한 정보를 성공적으로 스크랩할 때마다 다음과 같이 콘솔에 정보를 인쇄합니다.

New article found for topic: python
URL: http://example.com/examplepage.html
TITLE: Page TItle Here
BODY: Body content is here

 

먼저 검색어를 정하고 해당 검색어에 대해 모든 사이트를 반복하는 형태를 취하고 있습니다. 보통 검색어목록이 사이트 목록보다 적기 때문입니다.

 

사이트를 먼저 정하고 그 사이트에 대해 모든 검색어를 검생한 다음, 그 다음 웹사이트로 넘어가는 형태를 취했다면 각 서버에 대한 부하를 균등하게 배분하지 못했을 것입니다.