티스토리 뷰

python/웹 크롤링(python)

크롤링의 시작

취뽀가자!! 2018. 5. 2. 23:34

 단일 도메인 내의 이동

웹 크롤러라는 이름은 웹을 크롤링하기 때문에 붙여진 이름입니다. 그 핵심은 재귀입니다. URL에서 페이지를 가져오고, 그페이지를 검사해 다른 URL을 찾고, 다시 그 페이지를 가져오는 작업을 무한히 반복하는 것이 웹 크롤러라고 할 수 있습니다.

URL에서 페이지를 가져오고, 그 페이지를 검사해 다른 URL를 찾는 방법은 '케빈 베이컨의 여섯 다리'를 이용할 것입니다. 서로 목표가 관계없어 보이는 두 대상을 연결시키겠다는 것입니다.(링크로 연결된 항목들)

아래 스크립트는 위키백과 페이지를 가져와서 페이지에 들어 있는 링크 목록을 가져오는 스크립트입니다

1
2
3
4
5
6
7
8
from urllib.request import urlopen
from bs4 import BeautifulSoup
 
html=urlopen("http://en.wikipedia.org/wiki/kevin_Bacon")
bsObj=BeautifulSoup(html,"html.parser")
for link in bsObj.findAll("a"):
    if 'href' in link.attrs:
        print(link.attrs['href'])
cs


링크 목록을 살펴보면 에상대로(예상..??) 'Apollo 13','Philadelphia','Primetime Emmy Award' 등이 모두 있을 것입니다. 하지만 원하지 않는 것들도 포함되어 있을 것입니다.


그 이유는 위키백과의 모든 페이지에는 사이드바, 푸더, 헤더 링크가 있고 카테고리 페이지, 토론 페이지 등 그 외에도 우리가 관심 있어 하는 항목이 아닌 페이지를 가리키는 링크가 많이 있습니다. 하지만 항목 페이지를 가리키는 링크에는 다른 내부 페이지를 가리키는 링크와 비교되는 세 가지 공통점을 찾을 수 있습니다.


● 이 링크들은 id가 bodyContent인 div 안에 있습니다.

● URL에는 세미콜론이 포함되어 있지 않습니다.

● URL은 /wiki/로 시작합니다.


이 규칙들을 활용하면 항목 페이지를 가리키는 링크만 가져오도록 할 수 있습니다.


1
2
3
4
5
6
7
8
9
10
from urllib.request import urlopen
from bs4 import BeautifulSoup
import re
 
html=urlopen("http://en.wikipedia.org/wiki/Kevin_Bacon")
bsObj=BeautifulSoup(html,"html.parser")
for link in bsObj.find("div",{"id":"bodyContent"}).findAll("a",
    href=re.compile("^(/wiki/)((?!:).)*$")):
    if 'href' in link.attrs:
        print(link.attrs['href'])
cs


이 코드를 실행해 보면 위키백과 항목에서 다른 항목을 가리키는 모든 링크 목록을 볼 수 있을 것입니다.


이번에는 위 코드를 아래 두 조건들을 추가하여 좀 더 향상된 코드를 만들 수 있습니다.


● /wiki/<article_name> 형태인 위키백과 항목 URL을 받고, 링크된 항목 URL 목록 전체를 반환하는 getLinks 함수

● 시작 항목에서 getLinks를 호출하고 반환된 리스트에서 무작위로 항목 링크를 선택하여 getLinks를 다시 호출하는 작업을, 프로그램을     끝내거나 새 페이지에 항목 링크가 없을 때까지 반복하는 메인 함수


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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"+articleUrl)
    bsObj=BeautifulSoup(html,"html.parser")
    return bsObj.find("div",{"id":"bodyContent"}).findAll("a",
        href=re.compile("^(/wiki/)((?!:).)*$"))
 
links=getLinks("/wiki/Kevin_Bacon")
while len(links)>0:
    newArticle=links[random.randint(0len(links)-1)].attrs['href']
    print(newArticle)
    links=getLinks(newArticle)
cs


이 프로그램이 필요한 라이브러리를 임포트한 후 처음 한 일은 현재 시스템 시간을 가지고 난수 발생기를 실행하는 겁니다. 이렇게 하면 프로그램을 실행할 때마다 위키백과 항목들 속에서 새롭고 흥미로운 무작위 경로를 찾을 수 있습니다.


그 다음 getLinks 함수를 정의합니다. 이 함수는 /wiki/....형태로 된 URL을 받고 그 앞에 위키백과 도메인 이름인http://en.wikipedia.org를 붙여, 그 위치의 HTML에서 BeautifulSoup 객체를 가져옵니다. 그리고 앞에서 설명한 매개변수에 따라 항목 링크 태그 목록을 추출해서 반환합니다.


이 프로그램은 초기 페이지인 https://en.wikipedia.org/wiki/Kevin_Bacon의 링크 목록을 links 변수로 정의하며 시작합니다. 그리고 루프를 실행해서 항목 링크를 무작위로 선택하고, 선택한 링크에서 href 속성을 추출하고, 페이지를 출력하고, 추출한 URL에서 새 링크 목록을 가져오는 작업을 반복합니다.


사실 단순히 페이지로 이동한다고 해서 케빈 베이컨의 여섯다리 문제가 풀리는 것이 아닙니다. 왜냐면 반드시 결과 데이터를 저장하고 분석할 수 있어야 하기 때문입니다.


다크 웹과 딥 웹

딥 웹(deep web), 다크 웹(dark web), 히든 웹(hidden web) 같은 용어를 최근 많이 들어봤을 겁니다. 

딥 웹은 간단히 말해 표준 웹(surface web), 즉 검색 엔진에서 저장하는 부분을 제외한 나머지 웹을 일컫습니다. 정확히 알 수는 없지만, 딥 웹은 틀림없이 인터넷의 9할 정도를 차지할 겁니다. 구글도 폼을 전송하거나, 최상위 도메인에서 링크되지 않은 페이지를 찾아내거나, robots.txt로 막혀 있는 사이트를 조사할 수는 없으므로, 표면 웹은 비교적 작은 비율을 차지합니다.

다크 웹은 다크넷, 다크 인터넷이라고도 불리며 완벽히 다른 죵류입니다. 다크 웹은 기존 네트워크 기반 구조에서 동작하기는 하지만, Tor클라이언트와 HTTP 위에서 동작하며 보안 채널로 정보를 교환하는 애플리케이션 프로토콜을 사용합니다. 다크 웹도 다른 웹사이트와 마찬가지로 스크랩할 수는 있지만 이 책의 범위는 벗어납니다.

다크 웹과 달리 딥 웹은 비교적 쉡기 스크랩할 수 있습니다. 

전체 사이트 크롤링

위에서는 링크에서 링크로 움직이며 웹사이트를 무작위로 이동했습니다. 하지만 사이트의 모든 페이지를 어떤 시스템에 따라 분류하거나 검색해야 한다면 이런 방식은 적합하지 않습니다.

그럼 웹사이트 전체 크롤링은 언제 유용하고, 언제 손해일까요? 사이트 전체를 이동하는 웹 스크레이퍼에는 여러 가지 장점이 있습니다. 

사이트맵 생성

우리가 사이트맵에 시스템 내부에 접근 권한이 없고 공개된 사이트맵도 없을 때 사이트 전체를 이동하는 크롤러를 이요해 내부 링크를 모두 수집하고, 그페이지들을 사이트의 실제 폴더 구조와 똑같이 정리할 수 있을 겁니다. 또 이를 통해 존재하는지조차 몰랐던 부분들들 발견할 수 있을 것이고, 다시 설계해야 하는 페이지가 얼마나 되고 이동해야 할 컨텐츠가 얼마나 되는지 정확하게 산출할 수 있을 것입니다.

데이터 수집

어떠한 사이트를 철저히 크롤링 할 때 보통 홈페이지와 같은 최상위 페이지에서 시작해, 그 페이지에 있는 내부 링크를 모두 검색합니다. 검색한 링크를 모두 탐색하고, 거기서 다시 링크가 발견되면 크롤링 다음 라운드가 시작됩니다.

모든 페이지에 내부 링크가 10개씩 있고 사이트가 다섯 단계로 구서오디어 있다면, 최소 105페이지에서 최대 1000,000페이지를 찾아야 사이트를 철저히 탐색했다고 할 수 있습니다. 이상하게도 '페이지당 내부 링크 10개'가 매우 일반적인 크기인데도 100,000페이지 를 찾아야 하는 이유는 물론 내부 링크가 상당수가 중복이기 때문입니다.

따라서 같은 페이지를 두 번 크롤링 하지 않으려면 발견되는 내부 링크가 모두 일정한 형식을 취하고, 프로그램이 동작하는 동안 계속 유지되는 리스트에 보관하는 게 대단히 중요합니다. 새로운 링크만 탐색하고 거기서 다른 링크를 검색해야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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)
    bsObj=BeautifulSoup(html,"html.parser")
    for link in bsObj.findAll("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("")
cs

*파이썬은 기본적으로 재귀 호출을 1000회로 제한합니다.

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

웹 크롤러가 페이지와 페이지 사이를 옮겨 다니기만 한다면 쓸모가 없을 겁니다. 쓸모가 있으려면 페이지에 머무르는 동안 뭔가 다른 일을 해야 합니다. 

그러기 위해선 가장 먼저 할 일은 사이트의 페이지 몇 개를 살펴보며 패턴을 찾는 일입니다.(아래 패턴은 위키백과의 패턴입니다)

● 항목 페이지든 편집 내역 페이지든 기타 무슨 페이지든 상관없이 제목은 항상 h1 태그 안에 있으며 h1태그는 페이지당 하나만 존재합니다.
● 이미 언급했던 모든 바디 텍스트는 div#bodyContent 태그에 들어 있습니다. 하지만 더 명확하게 첫 번째 문단의 텍스트만 선택하려 한다면     div#mw-content-text -> p로 첫 번째 문단 태그만 선택하는 편이 나을 수 있습니다. 이 방법은 콘텐츠 텍스트 섹션이 없는 파일 페이지(예를 들어 https://en.wikipedia.org/wiki/File:0rbit_of_2774301_Wikipedia.svg)를 제외한 모든 콘텐츠 페이지에 적용됩니다.
● 편집 링크는 항목 페이지에만 존재합니다. 존재한다면 li#ca-edit -> span -> a로 찾을 수 있습니다.

아래 코드는 페이지 제목, 첫 번째 문다. 편집 페이지를 가리키는 링크(존재한다면)를 수집하는 스크래이퍼입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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)
    bsObj=BeautifulSoup(html,"html.parser")
    try:
        print(bsObj.h1.get_text())
        print(bsObj.find(id=="mw-content-text").findAll("p")[0])
        print(bsObj.find(id=="ca-edit").find("span").find("a").attrs['href'])
    except AttributeError:
        print("This page is missing something! No worries though!")
    for link in bsObj.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("")
cs

원하는 데이터가 모두 페이지에 있다고 확신할 수는 없으므로, 각 print문은 페이지에 존재할 확률이 높은 순서대로 정렬했습니다. <h1>타이틀 태그는 모든 페이지에 존재하므로 이 데이터를 가장 먼저 가져옵니다. 파일 페이지를 제외하면, 대부분의 페이지에 텍스트 콘텐츠가 존재하므로 이것이 두 번째로 가져올 데이터입니다. 편집 버튼은 제목과 텍스트 콘텐츠가 모두 존재하는 페이지에만 있지만, 그렇다고 해도 100퍼센트는 아닙니다.

인터넷 크롤링

이번에는 외부 링크를 무시하지 않고 따라갈 겁니다. 
단순히 외부 링크를 닥치는대로 따라가는 크롤러를 만들기 전에 먼저 자신에게 다음과 같은 질문을 해보십시오.


● 내가 수집하려는 데이터는 어떤 것이지? 정해진 사이트 몇 개만 수집하면 되나? (이런 경우, 거의 틀림없이 쉬운 방법이 있습니다.) 아      니면 그런 사이트가 있는지조차 몰랐던 사이트에도 방문하는 크롤러가 필요할까?
● 크롤러가 특정 웹사이트에 도달하면, 즉시 새 웹사이트를 가리키는 링크를 따라가야 할까? 아니면 한동안 웹사이트에 머물면서 파고      들어야 할까?
● 특정 사이트를 스크랩에서 제외할 필요는 없나? 
● 만약 크롤러가 방문한 사이트의 웹마스터가 크롤러의 방문을 알아차렸다면 나 자신을 법적으로 보호할 수 있을까?


아래 프로그램은 http://oreilly.com에서 시작해 외부 링크에서 외부 링크로 무작위로 이동하는 스크립트 입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
from urllib.request import urlopen
from bs4 import BeautifulSoup
from urllib.parse import urlparse
import re
import datetime
import random
 
pages=set()
random.seed(datetime.datetime.now())
 
#페이지에서 발견된 내부 링크를 모두 목록으로 만듭니다.
def getInternetLinks(bsObj, includeUrl):
    includeUrl=urlparse(includeUrl).scheme+"://"+urlparse(includeUrl).netloc
    internetLinks=[]
    # /로 시작하는 링크를 모두 찾습니다.
    for link in bsObj.findAll("a",href=re.compile("^(/|.*"+includeUrl+")")):
        if link.attrs['href'is not None:
            if link.attrs['href'not in internetLinks:
                if(link.attrs['href'].startswith("/")):
                    internetLinks.append(includeUrl+link.attrs['href'])
                else:
                    internetLinks.append(link.attrs['href'])
    return internetLinks
 
# 페이지에서 발견된 외부 링크를 모두 목록으로 만듭니다.
def getExternalLinks(bsObj, excludeUrl):
    externalLinks=[]
    # 현재 URL을 포함하지 않으면서 http나 www로 시작하는 링크를 모두 찾습니다.
    for link in bsObj.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 externalLinks
 
def splitAddress(address):
    addressParts=address.replace("http://""").split("/")
    return addressParts
 
def getRandomExternalLink(startingPage):
    html=urlopen(startingPage)
    bsObj=BeautifulSoup(html,"html.parser")
    externalLinks=getExternalLinks(bsObj, urlparse(startingPage).netloc)
    if len(externalLinks)==0:
        domain=urlparse(startingPage).scheme+"://"+urlparse(startingPage).netloc
        internetLinks=getExternalLinks(bsObj,domain)
        return getRandomExternalLink(internetLinks[random.randint(0,len(internetLinks)-1)])
    else:
        return externalLinks[random.randint(0,len(externalLinks)-1)]
 
def followExternalOnly(startingSite):
    externalLink=getRandomExternalLink(startingPage)
    print("Random external link is: "+externalLink)
    followExternalOnly(externalLink)
 
 
followExternalOnly("http://oreilly.com")
cs

웹사이트의 첫 번째 페이지에 항상 외부 링크가 있다는 보장은 없습니다. 여기서는 외부 링크를 찾기 위해 이전 크롤링 예제와 비슷한 방법, 즉 외부 링크를 찾을 때까지 웹사이트를 재귀적으로 파고드는 방법을 썼습니다.

아래 순서도는 이러한 작업 내용을 시각한 것입니다.




작업을 '이 페이지에 있는 모든 외부 링크를 찾는다' 같은 단순한 함수로 나누면, 나중에 코드를 다른 크롤링 작업에 쓸 수 있도록 리팩토링하기 쉽습니다. 예를 들어 사이트 전체에서 외부 링크를 검색하고 각 링크마다 메모를 남기고 싶다면 다음과 같은 함수를 추가하면 됩니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 사이트에서 찾은 외부 URL을 모두 리스트로 수집
allExtLinks=set()
allIntLinks=set()
 
def getAllExteralLinks(siteUrl):
    try:
        html=urlopen(siteUrl)
    except HTTPError as e:
        return None
    try:
        bsObj=BeautifulSoup(html,"html.parser")
        internalLinks=getInternalLinks(bsObj,splitAddress(domain)[0])
        externalLinks=getExteranlLinks(bsObj,splitAddress(domain)[0])
    except AttributerError as e:
        return None
 
    for link in externalLinks:
        if link not in allExtLinks:
            allExtLinks.add(link)
            print(link)
    for link in internalLinks:
        if link=="/":
            link=domain
        elif link[0:2]=="//":
            link="http:"+link
        elif link[0:1]=="/":
            link=domain+link
 
        if link not in allIntLinks:
            print("About to get link:"+link)
            allIntLinks.add(link)
            getAllExteralLinks(link)
 
domain="http://oreilly.com"
getAllExteralLinks(domain)
 
cs

이 코드는 크게 루프 두 개로 생각할 수 있습니다. 하나는 내부 링크를 수집하고, 다른 하나는 외부 링크를 수집하면서 서로 연관되게 동작합니다. 순서도로 나타낸다면 아래 그림과 비슷할 것입니다.


리다리렉트 처리

리다이렉트를 사용하면 같은 웹 페이지를 다른 도메인 이름 아래에서 볼 수 있습니ㅏㄷ. 리다이렉트는 크게 두 가지입니다.

● 서버 쪽 리다이렉트, 페이지를 불러오기 전에 URL이 바뀝니다.
● 클라이언트 쪽 리다이렉트, 이따금 페이지를 리다이렉트하기 전에 'You will be directed in 10 seconds...'같은 메시지가 나올 때도 있      습니다.

서버 쪽 리다이렉트에 대해서는 보통 별로 신경 쓸 일이 없습니다. 파이썬 3.x에서 제공하는 urllib 라이브러리가 리다이렉트를 자동으로 해 주니깐요. 그저 이따금 크롤링하는 페이지 URL이 입력한 URL과 정확히 일치하지 않을 수 있다는 것만 기억하면 됩니다.











※출처(파이썬으로 웹 크롤러 만들기)























'python > 웹 크롤링(python)' 카테고리의 다른 글

API에서 얻은 데이터와 웹 스크레이퍼 결합하기!!!  (0) 2018.05.05
고급 HTML 분석  (0) 2018.05.01
BeautifulSoup 라이브러리  (0) 2018.05.01
urllib 라이브러리  (0) 2018.04.29
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함