웹 스크래핑 기초: 소스코드 읽기와 태그 찾기
*본 게시물은 21-2학기 연세대학교 일반대학원 온라인데이터수집과분석(이상엽 교수님) 수업 내용을 정리한 것입니다.
웹스크래핑은 흔히 '크롤링'으로 알고 있는 온라인 데이터 수집 방법입니다.
다만 스크래핑은 하나의 웹 페이지에 존재하는 정보를 가져오는 행위인 반면 크롤링은 전체 사이트를 긁어온다는 점에서 차이가 있습니다.
웹에는 굉장히 많은 데이터가 존재합니다. 웹 페이지들로 구성된 홈페이지는 서버에서 raw data인 소스코드를 받아서 사용자들에게 예쁘게 보여주는 역할을 합니다.(raw data인 소스코드는 chrome > '페이지 소스 보기'로 확인 가능합니다.) 소스코드는 다양한 language들로 작성됩니다. 특히 html이 많이 쓰입니다. html 소스코드는 특히 여러개의 태그로 구성됩니다. 가령 <title>제목</title> 이런 식으로 이루어집니다. 앞의 태그를 시작 태그(start tag), 뒤의 태그를 끝 태그(end tag)라 합니다.
소스코드는 파이썬 기본 모듈인 request 혹은 urllib을 사용해서 서버에서 다운로드 할 수 있습니다. 이렇게 다운받은 소스코드를 모듈 bs4의 BeautifulSoup 클래스를 활용해서 정보를 담고 있는 태그를 찾고, 정보까지 추출할 수 있습니다.
request 사용하기
request는 서버 주소(주로 웹 페이지 URL)에 접근하여 소스 코드를 가져옵니다. 가져올 때 반드시 http://를 붙여서 가져와야 합니다. request의 get()함수를 사용하며, 인자로는 URL이 옵니다. URL은 '서버 컴퓨터 주소(~.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
|
import requests
url = 'http://www.yes24.com/Product/Goods/102789938' #일단 get의 인자가 되는 url을 'url' 변수에 저장한 후,
r = requests.get(url)
print(r.text) # 아래 보이는 소스코드는 브라우저에서 보이는 '페이지 소스'보기와 거의 동일합니다.
# 소스코드에는 최종적으로 추출하고자 하는 정보들이 태그에 저장되어 있습니다.
'''
<!DOCTYPE html >
<html lang="ko">
<head><link rel ="canonical" href="http://www.yes24.com/Product/Goods/102789938"> <link rel ="alternate" media="only screen and(max-width: 640px)" href="http://m.yes24.com/Goods/Detail/102789938">
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<meta http-equiv="Accept-CH" content="dpr, width, viewport-width, rtt, downlink, ect, UA, UA-Platform, UA-Arch, UA-Model, UA-Mobile, UA-Full-Version" />
<meta http-equiv="Accept-CH-Lifetime" content="86400" />
<meta name="viewport" content="width=1170" />
<title> 달러구트 꿈 백화점 2 - YES24 </title>
<meta name="title" content="달러구트 꿈 백화점 2 - YES24"/>
<meta name="author" content="이미예" />
<meta name="description" content="100만 독자를 사로잡은 『달러구트 꿈 백화점』, 그 두 번째 이야기어느덧 페니가 달러구트 꿈 백화점에서 일한 지도 1년이 넘었다. 재고가 부족한 꿈을 관리하고, 꿈값 창고에서 감정으로 가득 찬 병을 옮기고, 프런트의 수많은 눈꺼풀 저울을 관리하는 일에 능숙해..."/>
<meta name="keywords" content="달러구트 꿈 백화점 2,EBS북카페,문학뉴스레터에소개된책,어른이를위한힐링책,힐링판타지,믿고보는시리즈,단골손님을 찾습니다, 이미예, 팩토리나인, 9791165343729, 116534372X9791165343729,116534372X"/>
'''
|
cs |
bs4의 BeautifulSoup 사용하기
그렇다면 찾고자 하는 태그와, 그 태그가 저장하고 있는 정보에는 어떻게 접근해야 할까요? bs4모듈의 BeautifulSoup를 사용합니다.
1) 태그 이름으로 접근하기
1
2
3
4
5
|
from bs4 import BeautifulSoup
soup = BeautifulSoup(r.text, 'lxml') #인자는 소스코드, 파서의 이름
soup.title #원하는 태그에 '태그의 이름'을 통해 접근 가능: <title> 역사의 역사 - YES24 </title>
#태그에 접근했으면 정보 추출
soup.title.text #' 역사의 역사 - YES24 ', str로 리턴
|
cs |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
# ' 역사의 역사 - YES24 ' 후처리
title = soup.title.text
# 공백제거엔 strip
title = title.strip() #'역사의 역사 - YES24'
# -YES24 지우기: #split: 특정 문자열 값을 가지고 주어진 문자열 데이터를 쪼개고자 할 때
title.split('-') # 리스트['역사의 역사 ', ' YES24']
title.split('-')[0] #'역사의 역사 '
title.split('-')[0].strip() #마지막 공백까지 삭제, '역사의 역사'
title = title.split('-')[0].strip() #'역사의 역사'
#한글에 대해서 utf-8 인코딩이 제일 많이 사용
with open('results.txt', 'w', encoding='utf-8') as f:
f.write(title)
|
cs |
2) 이름이 똑같은 태그가 여러개 존재할 때, 그리고 속성 정보로도 태그를 찾고 싶을 때
태그 이름으로 접근하는 것의 한계는 동일한 이름을 가진 태그가 여러개 존재할 수 있다는 것입니다.
가령 다음과 같은 html_test.html이란 파일을 보면...
이름이 p인 태그는 <p class='title'> 부터 시작해서 <p class= 'description>, <p>...에 이르기까지 매우 빈번하게 나오고 있습니다. 이 html 파일을 일단 파이썬으로 불러오도록 하겠습니다.
1
2
3
4
|
# 예시 파일 html_test.html 불러오기
# URL에 있는 html 소스코드는 request의 get()을 사용해서 가져와야 합니다.
with open('C:?Users/사용자이름/폴더1/html_test.html', 'r', encoding = 'utf8') as f:
html = f.read()
|
cs |
이렇게 읽어온 html 파일을 html이라는 변수에 저장했습니다. BeautifulSoup를 통해 태그 탐색을 실행해보도록 하겠습니다. 보통의 태그 이름으로 접근하는 방법입니다.
1
2
3
4
5
6
|
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'lxml')
# 태그 이름으로 접근하기의 경우_ 태그가 하나 있을 때
soup.title #<title> Online Data Scraping Test</title>
title = soup.title.text # ' Online Data Scraping Test'
title = title.strip() #'Online Data Scraping Test'
|
cs |
그렇다면 태그가 여러번 나올 때 태그 이름을 쓰면 어떻게 될까요?
1
|
soup.p #<p class="title">HTML code for the lecture</p>
|
cs |
제일 첫 번째 태그밖에 표출이 되지 않습니다. 때문에 다른 방법인 find()와 find_all()의 방법을 사용해야 합니다. 이 둘은 태그의 이름 혹은 태그의 속성 정보(=attribute, 추가적 정보)를 받습니다. find()의 경우 인자에 해당되는 첫 번째 태그만, find_all()은 인자에 해당되는 모든 태그를 리스트로 반환합니다.
인자로 우선 이름을 주겠습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
soup.find('p') # 태그 이름이 p인 것들 중 첫 번쨰 태그만 반환
# <p class="title">HTML code for the lecture</p>
soup.find_all('p') # 태그 이름이 p인 것들을 모두 찾은 후 리스트로 반환
'''
[<p class="title">HTML code for the lecture</p>,
<p class="description">This code will be used as HTML source code for the BS lecture. For more detailed information about BS, you can click
<a class="BS_English" href="http://www.pythonforbeginners.com/python-on-the-web/beautifulsoup-4-python/"> BS4 for beginners</a></p>,
<p class="description">한국어로 된 정보를 원한다면
<a class="BS_Korean" href="http://coreapython.hosting.paran.com/etc/beautifulsoup4.html"> BS4 for beginners (한글버전)</a>을 이용하세요</p>,
<p><span class="red_price">35.6</span></p>,
<p><span class="blue_price">43.2</span></p>,
<p><span method="manual">24.3</span></p>]
'''
len(soup.find_all('p')) #원소는 6개
soup.find_all('p')[4] #<p><span class="blue_price">43.2</span></p>
soup.find_all('p')[-1] #<<p><span method="manual">24.3</span></p> 음의 인덱스를 사용하여 가장 마지막에 접근
# 가장 마지막 <p>태그의 정보는 가격입니다. 문자열로 나타납니다.
# 얘를 숫자로 바꾸어 연산해보습니다.
price = soup.find_all('p')[-1].text #'24.3', str 타입
price = float(price) #24.3으로 환원, int로 환원 시 오류남
price+10 #34.3
|
cs |
인자 중에는 속성(attribute) 정보도 있다고 했습니다. 만일 태그<p class='title'>가 있다면 class는 속성 정보, 구체적 값은 'title'이 됩니다. find()와 find_all()에 속성 정보를 넣어 검색하는 코드는 다음과 같습니다.
soup.find('태그이름', attrs = {'속성정보':'값'}) soup.find_all('태그이름', attrs = {'속성정보':'값'}) |
위와 동일하게 find()는 첫 번째 값만, find_all()은 모든 값을 리스트로 반환합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
#태그가 가진 정보에는 이름 외에도 많음: 속성(attribute)정보
# 예: p 태그- 엄청 많음! <p class='title'>중 class가 속성 정보, 구체적 값은 'title'
# <p class="description">: class는 속성 정보, 값은 ;description'
#find()의 attr 파라미터 사용
# soup.find('tag name', attrs = {'attr_name':'value'})
soup.find('p', attrs= {'class':'title'} )
soup.find('p', attrs= {'class':'description'} ) # find()는 태그의 첫 번째 값만 반환
soup.find_all('p', attrs= {'class':'description'} ) # find_all()는 모든 태그를 리스트로 반환
'''
[<p class="description">This code will be used as HTML source code for the BS lecture. For more detailed information about BS, you can click
<a class="BS_English" href="http://www.pythonforbeginners.com/python-on-the-web/beautifulsoup-4-python/"> BS4 for beginners</a></p>,
<p class="description">한국어로 된 정보를 원한다면
<a class="BS_Korean" href="http://coreapython.hosting.paran.com/etc/beautifulsoup4.html"> BS4 for beginners (한글버전)</a>을 이용하세요</p>]
'''
soup.find('p', attrs= {'class':'title'} ).text #'HTML code for the lecture'
soup.find('p', attrs= {'class':'description'} ).text
#'This code will be used as HTML source code for the BS lecture. For more detailed information about BS, you can click\n\t\t\t BS4 for beginners'
soup.find_all('p', attrs= {'class':'description'} )[-1].text # find_all()는 모든 태그를 리스트로 반환. 인덱싱 가능
#'한국어로 된 정보를 원한다면\n\t\t\t BS4 for beginners (한글버전)을 이용하세요'
|
cs |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
# 이번에는 a태그를 찾아봅시당
soup.find('a')
soup.find_all('a')
'''
[<a class="BS_English" href="http://www.pythonforbeginners.com/python-on-the-web/beautifulsoup-4-python/"> BS4 for beginners</a>,
<a class="BS_Korean" href="http://coreapython.hosting.paran.com/etc/beautifulsoup4.html"> BS4 for beginners (한글버전)</a>]
'''
soup.find_all('a')[-1]
#<a class="BS_Korean" href="http://coreapython.hosting.paran.com/etc/beautifulsoup4.html"> BS4 for beginners (한글버전)</a>
# 속성 정보로 찾기
soup.find('a', attrs= {'class':'BS_Korean'} )
soup.find_all('a', attrs= {'class':'BS_Korean'}) #항상 리스트로 반환, 이 뒤에 .text 안됨
soup.find_all('a', attrs= {'class':'BS_Korean'})[-1].text #대신 리스트에서 원소를 인덱싱으로 꺼내는 경우 .text 가능
#' BS4 for beginners (한글버전)'
|
cs |
3) BeautifulSoup 의 get(): 속성 정보의 값을 뽑고 싶을 때
위에서 계속 썼던 .text의 경우 <시작태그>...</끝태그> 사이의 text 정보만을 찾아줬습니다. 이는 웹 페이지에서 추출하려는 정보가 text인 경우에 쓸 수 있으나, 추출하려는 정보가 text가 아닌 속성 정보 값이 경우도 존재합니다.
가령 다음과 같은 내용이 있을 때 'href' 정보만 뽑고 싶다면 어떻게 할까요?
<a class="BS_English" href="http://www.pythonforbeginners.com/python-on-the-web/beautifulsoup-4-python/"> BS4 for beginners</a></p> <p class="description">한국어로 된 정보를 원한다면 <a class="BS_Korean" href="http://coreapython.hosting.paran.com/etc/beautifulsoup4.html"> BS4 for beginners (한글버전)</a> |
이 때, 즉 특정 태그가 가진 특정 속성 정보를 가져올 때 BeautifulSoup의 get()을 사용하게 됩니다. get()은 인자로 속성 정보 이름을 받습니다.
순서는 특정 태그찾기 -> get()을 사용해서 인자에 속성 정보 이름을 넣은 후 -> 속성 정보 값 추출하기가 됩니다.
1
2
3
4
5
6
7
8
9
10
|
# 특정 태그를 일단 찾은 후
# 그 태그에 get()을 적용하여 인자에 속성 정보 이름을 넣은 후, 그 속성 정보의 값을 추출
# 예) href: 하이퍼링크의 주소 찾기
soup.a # 첫번쨰 a태그 에서만 속성 정보의 값을 찾는 경우 이렇게 하기가 가능
soup.a.get('href')#'http://www.pythonforbeginners.com/python-on-the-web/beautifulsoup-4-python/'
# 태그가 첫 번쨰가 아닌 중간에 껴 있을 때
soup.find('a', attrs={'class':'BS_Korean'}).get('href') #'http://coreapython.hosting.paran.com/etc/beautifulsoup4.html'
soup.find('a', attrs={'class':'BS_English'}).get('href') #''http://www.pythonforbeginners.com/python-on-the-web/beautifulsoup-4-python/'
|
cs |
4) 메타 정보가 2개 이상인 경우
수업에서는 YES24 홈페이지에서 웹 페이지에 나온 책 제목 정보를 추출하는 연습을 했었습니다. 여기에는 몇 가지 변수들이 있었습니다.
첫 번째로 태그의 구성이 속성정보 1: 값 1이 아니란 것입니다. 즉 책 제목이 들어간 태그들을 찾을 때 <meta name="title" content="갯마을 차차차 2 - YES24"/>와 같이 속성 정보가 2개인 것을 고려해야 했습니다.
주의할 점은 이 중에 찾아야 하는 정보는 꼭 meta 태그 중 content 속성 정보 이름(클래스)의 값이 "갯마을 차차차 2 - YES24"같지만!!! content의 경우 책이 바뀔 때마다 attribute 값을 바꿔주어야 하므로 모든 메타 태그 이름에 동일하게 들어간 title을 찾아야 합니다. 즉 태그 별로 name="title"은 계속 겹치므로, 책제목이 들어간 태그를 찾을 수 있습니다.
1
2
3
4
5
6
7
|
url2 = requests.get('http://www.yes24.com/Product/Goods/104105224') #웹페이지 소스코드 다운
soup = BeautifulSoup(url2.text, 'lxml')
# 원하는 태그를 찾기 위해 BeautifulSoup 사용
soup.find('meta', attrs={'name':'title'}) #<meta content="갯마을 차차차 2 - YES24" name="title"/>
#의외로 content가 아님! content로 할 경우 책이 바뀔 때마다 attribute 값을 바꿔주어야 함
# <meta content="갯마을 차차차 2 - YES24" name="title"/>
|
cs |
태그들을 찾았으면 이제 책 제목을 찾아야 합니다. 태그의 속성 정보 이름에 딸린 속성 정보 값을 찾는 것이므로 BeautifulSoup의 get()을 사용해야 합니다.
1
2
3
4
|
# <meta content="갯마을 차차차 2 - YES24" name="title"/> 중 content의 값을 찾고자 함
#특정 속성을 찾기 위해 get() 사용
title = soup.find('meta', attrs={'name':'title'}).get('content') #'갯마을 차차차 2 - YES24'
title = title.split('-')[0]
|
cs |
Exercise
수업에서는 YES24 웹 페이지에서 책제목, 저자, 판매가를 추출하고, 이를 텍스트 파일로 저장하는 실습을 했었습니다. 이에 해당하는 코드는 다음과 같습니다.
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
|
# http://www.yes24.com/Product/Goods/96971128
# 책제목, 저자, 판매가 추출한 후 텍스트 파일에 저장
# 1. url 가져오기 by request.get()
url3 = 'http://www.yes24.com/Product/Goods/96971128'
url3 = requests.get(url3)
# 2. BeautifulSoup를 통해 태그에 접근 준비
soup = BeautifulSoup(url3.text, 'lxml')
# 3. 웹 페이지에서 태그를 찾아 정보 추출하기
title3= soup.find('meta', attrs={'name':'title'}).get('content').split('-')[0]
author3 = soup.find('meta', attrs={'name':'author'}).get('content').split('-')[0]
prices3 = soup.find('em', attrs={'class':'yes_m'}).text
print(title3, author3, prices3) #어떤 죽음이 삶에게 말했다 김범석 15,000원
a = [title3, author3, prices3]
len(a)
# 4. 반복문을 통해 모든 정보를 txt 파일에 저장하기
for i in a:
infos = i+'\n'
with open('bookinfos.txt', 'a', encoding = 'utf8') as f:
f.write(infos)
f.close()
# 교수님꼐서 주신 정답
ttle = soup.title.text
ttle = ttle.split('-')[0].strip()# 제일 접근하기 쉬운 태그를 결정할 것
aathor = soup.find('meta', attrs={'name':'author'}).get('content')
pprice = soup.find_all('span', attrs={'class':'nor_price'})[0].text # '13,500원'
with open('yes24_result.txt', 'w', encoding='utf8') as f:
f.write('title'+'\t'+'author'+'\t'+'price'+'\n') # 헤더 정보 넣기
f.write(ttle+'\t'+aathor+'\t'+pprice+'\n')
f.close()
|
cs |
태그에 간접적으로 접근하기 - BeautifulSoup Navigation
BeautifulSoup Navigation 방법은 최종적으로 추출해야 하는 정보를 담은 태그에, 위의 방법들 처럼 직접적으로 접근하기 어려울 때 사용합니다. 따라서 그 주변 태그 이름에 직접 접근한 후, 주변 태그를 스타팅 포인트로 하여 최종 정보에 접근합니다. 부모-자식, 형제자매 관계를 사용하기 때문에 hierarchical한 구조를 사용한다고 이야기할 수 있습니다.
아까와 같이 동일한 html에 대해 작업을 수행합니다. 때문에 우선 html을 파이썬으로 불러와야 합니다.
1
2
3
4
5
|
# 예시 파일 html_test.html 불러오기
# URL에 있는 html 소스코드는 request의 get()을 사용해서 가져와야 합니다.
with open('C:?Users/사용자이름/폴더1/html_test.html', 'r', encoding = 'utf8') as f:
html = f.read()
soup = BeautifulSoup(html, 'lxml')
|
cs |
불러왔으면 스타팅 포인트로 삼는 태그를 만들어야 합니다.
첫 번째로는 title 이름을 가진 태그를 스타팅 포인트로 삼았습니다. 이후에 부모, 자식, 형제자매 관계로 접근을 하게 됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
# 스타팅 포인트 태그 삼기
soup.title # <title> Online Data Scraping Test</title>
# 부모 찾기
soup.title.parent # 부모 찾기
'''
<head>
<title> Online Data Scraping Test</title>
</head>
'''
# 자식 태그 + 다른 자식 element도 리턴; '\n'
soup.head.contents
# ['\n', <title> Online Data Scraping Test</title>, '\n'] 서로간은 형제, 자매 관계
'''
즉, <title> Online Data Scraping Test</title> 이라는 자식 태그 말고도,
그 자식 태그와 동일한 위계를 가지는 공백기호를 자식으로 취급합니다.
=============
<head>\n
<title> Online Data Scraping Test</title>\n
</head>
=============
이러한 생김새를 가지고 있었기 때문입니다.
|
cs |
다른 예로는 p태그를 가진 태그를 대상으로 형제 자매 관계를 보려고 합니다. 형제자매 태그는 hierarchy 내 동일한 위계에 있는 태그들 간의 관계입니다.
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
|
'''
<body>\n
<p class="title">HTML code for the lecture</p>\n
<p class="description">This code will be used as HTML source code for the BS lecture. For more detailed information about BS, you can click
<a class="BS_English" href="http://www.pythonforbeginners.com/python-on-the-web/beautifulsoup-4-python/"> BS4 for beginners</a></p>
'''
# 첫 번째 p 태그 찾기: 스타팅 포인트
soup.p #<p class="title">HTML code for the lecture</p>
# 태그 열리고 닫힌 후 가장 첫 번째의 형제자매 element를 리턴
soup.p.next_sibling #'\n'
'''
<body>\n
<p class="title">HTML code for the lecture</p>\n
<p class="description">This code will be used....
'''
# element 다음 형제자매 element인 p 태그 나옴
soup.p.next_sibling.next_sibling
'''
결과:
<p class="description">This code will be used as HTML source code for the BS lecture. For more detailed information about BS, you can click
<a class="BS_English" href="http://www.pythonforbeginners.com/python-on-the-web/beautifulsoup-4-python/"> BS4 for beginners</a></p>
'''
# 스타팅 포인트 기준 다시 이전 형제자매 태그로 이동
soup.p.previous_sibling #'\n'
|
cs |