본문 바로가기
IT/Scraping

크롤링 시작하기

by Cyber_ 2025. 2. 14.

웹 크롤링의 핵심은 재귀입니다. 웹 크롤러의 URL에서 페이지를 가져오고, 그 페이지를 검사해 다른 URL을 찾고, 다시 그 페이지를 가져오는 작업을 무한히 반복합니다. 웹 크롤러를 사용할 때는 반드시 대역폭에 세심한 주의를 기울여야 하며, 타깃 서버의 부하를 줄일 방법을 강구해야 합니다.

 

대역폭이란?
통신 네트워크에서 데이터가 전송될 수 있는 최대 전송 용량을 의미합니다. 즉, 단위 시간당 전송할 수 있는 데이터의 양입니다. 이는 보통 단위(mbps,gbps)로 측정 됩니다.

 

1. 단일 페이지 이동

만약 위키백과 페이지를 가져와서 페이지에 들어있는 링크 목록을 가져오는 파이썬 스크립트에서 해당페이지에 링크목록을 가져오는 것의 규칙을 발견한다면 "정규식"을 활용할 수 있습니다.

공통점3가지

  • 링크 id가 bodyContent인 div안에 있다.
  • URL에는 콜론이 포함되어 있다.
  • URL은 /wiki/로 시작한다.

위와 같이 공통점이 3가지 있다면 ^(/wiki/)((?!:).) 로 표현할 수 있습니다.

from urllib.request import urlopen
from bs4 import BeautifulSoup
import re

html = url('http://en.wikipedia.org/wiki/KevinBacon')
bs = BeautifulSoup(html, 'html.parser')
for link in bs.find('div', {'id': bodyContent'}).findAll('a',
                    href=re.compile('^(/wiki/)((?!:).)*$')):
    if 'href' in link.attrs:
        print(link.attrs['href'])

이를 활용하여 무작위로 항목 링크 선택하여 호출하는 함수를 만들어 링크가 없을 때까지 반복하는 함수를 작성하면 쓸모가 있어집니다.

파이썬의 의사 난수 발생기는 메르센 트위스터 알고리즘(Mersenne Twister algorithm)을 사용합니다. 이 알고리즘은 예측하기 어렵고 균일하게 분산된 난수를 만들긴 하지만, 프로세서 부하가 있는 편입니다.

from urllib.request import urlopen
from bs4 import BeautifulSoup
import datetime
import random
import re

random.seed(datetime.datetime.now())

def getLinks(articleUrl):
    html = urlopen('http://en.wikipedia.org{}'.format(articleUrl))
    bs = BeautifulSoup(html, 'html.parser')
    return bs.find('div', {'id':'bodyContent'}).findAll('a',
                    href = re.compile('^(/wiki/)((?!:).)*$'))

links = getLinks('/wiki/Kevin_Bacon'0
while len(links) > 0:
    newArticle = links[random.randint(0, len(links - 1)].attrs['href']
    print(newArticle)
    links = getLinks(newArticle)

데이터를 가져오는 것은 시작일 뿐입니다. 데이터를 저장하고 분석할 수 있어야합니다. 이 과정에 대해 학습할 필요성이있습니다.

 

2. 전체 사이트 크롤링

사이트 전체 크롤링, 특히 거대한 사이트의 크롤링은 메모리를 많이 요구하며 크롤링 결과를 바로 저장할 데이터베이스가 준비된 애플리케이션이 적합합니다.

다크웹과 딥웹

  • 딥웹: 표면웹, 즉 검색 엔진에서 저장하는 부분을 제외한 나머지 웹을 일컫습니다.
  • 다크 웹: 다크넷이라고도 불리우며, 기존 네트워크 하드웨어 인프라에서 동작하기는 하지만, Tor 같은 클라이언트를 사용하고 HTTP 위에서 동작하며 보안 채널로 정보를 교환하는 애플리케이션 프로토콜을 사용합니다.
  • 딥 웹: 입웹은 비교적 쉽게 스크랩할 수 있습니다. 구글 봇이 검색할 수 없는 곳을 탐색하고 스크랩하는 여러 도구가 있습니다.

사이트 전체를 이동하는 웹 스크레이퍼의 장점

  • 사이트 맵 생성
    공개된 사이트맵이 없는 상황에서 사이트 전체를 이동하는 크롤러를 이용해 내부 링크를 모두 수집하고, 그 페이지들을 사이트의 실제 폴더 구조와 똑같이 정리할 수 있습니다. 이를 통해 존재하는지조차 몰랐던 부분들을 빨리 발견할 수 있었고, 다시 설계해야 하는 페이지가 얼마나 되고 이동해야할 콘텐츠가 얼마나 이동되는지 정확히 산출할 수 있습니다.
  • 데이터 수집
    각 사이트를 재귀적으로 이동하는 크롤러를 이용하여 모든 링크롤 조사할 수 있습니다.

중복되는 링크들

같은 페이지를 두 번 크롤링하지 않으려면 발견되는 내부 링크가 모두 일정한 형식을 취하고, 프로그램이 동작하는 동안 계속 유지되는 세트에 보관하는게 중요합니다.

from urllib.request import urlopen
from bs4 import BeautifulSoup
import re

pages = set()
def getLinks(pageUrl):
    global pages
    html = urlopen('http://enwikipedia/org{}'.format(pageUrl))
    bs = BeautifulSoup(html, 'html.parser')
    for link in bs.find(All('a', href=re.compile('^(/wiki/)')):
        if 'href' in link.attrs:
            if link.attrs['href'] not in pages:
                #새 페이지 발견
                newPage = link.attrs['href']
                print(newPage)
                pages.add(newPage)
                getLinks(newPage)

getLinks('')

파이썬은 기본적으로 재귀 호출을 1,000회로 제한합니다. 멈추는 일을 예방하려면, 재귀 카운터를 삽입하거나 다른 방법을 강구해야 합니다.

 

전체 사이트에서 데이터 수집

가장 첫번째 할일은 사이트의 페이지 몇개를 살펴보며 패턴을 찾는 일입니다. 기본 크롤링 코드를 수정해서 크롤러와 데이터 수집(최소한 출력은 가능한) 기능이 있는 프로그램을 만들 수 있습니다.

from urllib.request import urlopen
from bs4 import BeautifulSoup
import re

pages = set()
def getLinks(pageUrl):
    global pages
    html = urlopen('http://en.wikipedia.org' + pageUrl)
    bs = BeautifulSoup(html, 'html.parser')
    try:
        print(bs.h1.get_text())
        print(bs.find(id = 'mw-content-text').findAll('p')[0])
        print(bs.find(id = 'ca-edit').find('span').find('a').attrs['href'])
        except AtrributError:
            print('This page is missing something! No worries thoght!')
        for link in bs.findAll('a', href = re.compile('^(/wiki/)')):
            if 'href' in link.attrs:
                if link.attrs['href'] not in pages:
                    newPage = link.attrs['href']
                    print('----------------\n' + newPage)
                    pages.add(newPage)
                    getLinks(newPage)

getLinks('')

패턴에 따라 필요한 작업이 다릅니다. 어떤 행에서 예외가 일어날지 모릅니다. 또한, 어떤 이유로든지 페이지에 편집 버튼만 있고 제목이 없다면 편집 버튼도 가져오지 않게 됩니다. 하지만 원하는 데이터가 사이트에 있을 확률에 순서가 있고, 일부 데이터를 잃어도 되거나 자셓나 로그를 유지할 필요가 없는 경우에는 별다른 문제가 없습니다.

 

인터넷 크롤링

구글은 다음과 같이 만들어졌다고 합니다.

  1. 수십억 달러를 모아 세계에서 가장 훌륭한 데이터센터를 만들고 세계 곳곳에 배치합니다.
  2. 웹 크롤러를 만듭니다.

다음 프로그램은 http://oreilly.com에서 시작해 외부 링크에서 외부 링크로 무작위로 이동합니다.

from urllib.request import urlopen
from bs4 import BeautifulSoup
import re
import datetime
import random

pages = set()
random.seed(datetime.datetime.now())

# 페이지에서 발견된 내부 링크를 모두 목록으로 만듭니다.
def getInternalLinks(bs, includeUrl)
    includeUrl = '{}://{}'.format(urlparse(includeUrl)
    internalLinks = []
    for link in bs.findAll('a', href = re.compile('^(/|.*' + includeUrl + ')')):
        if link.attrs['href'] is not None:
            if link.attrs['href'] not in internalLinks:
                if link.attrs['href'].startswith('/')):
                    internalLinks.append(includeUrl+link.attrs['href']
                else:
                    internalLinks.append(link.attrs['href'])
    return internalLinks

# 페이지에서 발견된 내부 링크를 모두 목록으로 만듭니다.
def getExternalLinks(bs, excludeUrl):
    externalLinks = []
    # 현재 URL을 포함하지 않으면서 http나 www로 시작하는 링크를 모두 찾습니다.
    for link in bs.findAll('a',
        href = re.compile('^{http|www)((?!' +excludeUrl + ').)*$')):
        if link.attrs['href'] is not None:
            if link.attrs['href'] not in externalLinks:
                externalLinks.append(link.attrs['href'])
    return externalLink

def getRandomExternalLink(startingPage):
    html = urlopen(startingPage)
    bs = BeautifulSoup(html, 'html.parser')
    externalLinks = getExternalLinks(bs, urlparse(atrtingPage).netloc)
    if len(externalLinks) == 0:
        print('No external links, looking around the site for one')
        domain= '{}://{}'.format(urlparse(startingPage).scheme,
            urlparse(startingPage).netloc)
        internalLinks = getInternalLinks(bs, domain)
        reutrn getRandomExteranlLink(internalLinks[random.randint(0, len(internalLinks)-1])
    else:
        return externalLinks[random.randint(0, len(externalLinks)-1)]

def followExternalOnly(startingSite):
    externalLink = getRandomExternalLink(startingSite)
    print('Random external link is: {}'.format(externalLink))
    followExternalOnly(externalLink)

followExternalOnly('http://oreilly.com')

 

Reference

Web Scraping with Python(Ryan Mitchell)